Spring Boot を使用して TiDB アプリケーションを構築する

このチュートリアルでは、TiDB を使用してスプリングブーツの Web アプリケーションを構築する方法を示します。 春のデータ JPAモジュールは、データ アクセス機能のフレームワークとして使用されます。このサンプル アプリケーションのコードはGitHubからダウンロードできます。

これは RESTful API を構築するためのサンプル アプリケーションであり、 TiDBをデータベースとして使用する汎用Spring Bootバックエンド サービスを示しています。次のプロセスは、実際のシナリオを再現するように設計されています。

これは、各プレイヤーがcoinsgoodsの 2 つの属性を持つゲームの例です。各プレイヤーはidフィールドで一意に識別されます。十分なコインと商品があれば、プレーヤーは自由に取引できます。

この例に基づいて独自のアプリケーションを構築できます。

ステップ 1: TiDB クラスターを起動する

以下にTiDBクラスターの起動方法を紹介します。

TiDB Cloudの無料クラスターを使用する

詳細な手順については、 無料のクラスターを作成するを参照してください。

ローカル クラスターを使用する

詳細な手順については、 ローカル テスト クラスターをデプロイするまたはTiUP を使用して TiDBクラスタをデプロイするを参照してください。

無料のクラスターを作成するを参照してください。

ステップ 2: JDK をインストールする

コンピュータにJava Development Kit (JDK) をダウンロードしてインストールします。 Java 開発に必要なツールです。 Spring Bootは、Java 8 以降のバージョンの JDK をサポートしています。ただし、 Hibernateのバージョンにより、Java 11 以降のバージョンの JDK を使用することをお勧めします。

Oracle JDKOpenJDKの両方がサポートされています。ご自身の判断でお選びいただけます。このチュートリアルでは、 OpenJDKの JDK 17 を使用します。

ステップ 3: Maven をインストールする

このサンプル アプリケーションでは、 Apache Mavenを使用してアプリケーションの依存関係を管理します。 Spring は Maven 3.3 以降のバージョンをサポートします。依存関係管理ソフトウェアとして、 Mavenの最新の安定バージョンをお勧めします。

コマンドラインからMavenをインストールするには。

  • マックOS:

    brew install maven
    
  • Debian ベースの Linux ディストリビューション (Ubuntu など):

    apt-get install maven
    
  • Red Hat ベースの Linux ディストリビューション (Fedora、CentOS など):

    • dnf:

      dnf install maven
      
    • うーん:

      yum install maven
      

その他のインストール方法については、 Mavenの公式ドキュメントを参照してください。

ステップ 4: アプリケーション コードを取得する

サンプル コード リポジトリをダウンロードまたは複製して、 spring-jpa-hibernateディレクトリに移動します。

同じ依存関係を持つ空のアプリケーションを作成する (オプション)

このアプリケーションは春の初期化を使用して構築されています。次のオプションをクリックし、いくつかの構成項目を変更することで、このサンプル アプリケーションと同じ依存関係を持つ空のアプリケーションをすばやく取得できます。

計画

  • Maven プロジェクト

言語

  • ジャワ

スプリングブーツ

  • 3.0.0-M2

プロジェクトのメタデータ

  • グループ: com.pingcap
  • アーティファクト: spring-jpa-hibernate
  • 名前: spring-jpa-hibernate
  • パッケージ名: com.pingcap
  • 包装: 瓶
  • ジャワ:17

依存関係

  • 春のウェブ
  • 春のデータ JPA
  • MySQLDriver

完全な構成は次のとおりです。

Spring Initializr Configuration

ノート:

SQL は比較的標準化されていますが、各データベース ベンダーは ANSI SQL 定義の構文のサブセットとスーパーセットを使用しています。これは、データベースの方言と呼ばれます。 Hibernate は、そのorg.hibernate.dialect.Dialectのクラスと各データベース ベンダーのさまざまなサブクラスを通じて、これらのダイアレクト全体のバリエーションを処理します。

ほとんどの場合、Hibernate は、ブートストラップ中に JDBC 接続に関するいくつかの質問をすることで、使用する適切なダイアレクトを決定できます。使用する適切な方言を決定する Hibernate の機能 (およびその解決に影響を与える機能) については、 方言解決を参照してください。

何らかの理由で適切な方言を決定できない場合、またはカスタム方言を使用したい場合は、 hibernate.dialectの設定を行う必要があります。

—— Hibernate の公式ドキュメントからの抜粋: Database Dialect

構成後、プロジェクトは通常どおり使用できますが、MySQL で使用するのと同じ方法、つまりMySQL ダイアレクトを使用する場合に限られます。これは、 Hibernate6.0.0.Beta2以降のバージョンでTiDB ダイアレクトをサポートするためですが、Hibernate での Spring Data JPA のデフォルトの依存関係は5.6.4.Finalです。したがって、 pom.xmlに対して次の変更を行うことをお勧めします。

  1. この依存ファイルに示すように、 Spring Data JPAで導入されたjakartaのパッケージを除外します。

    依存ファイルを次から変更します。

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    

    に:

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core-jakarta</artifactId>
        </exclusion>
    </exclusions>
    </dependency>
    
  2. この依存ファイルに示すように、 6.0.0.Beta2以降のバージョンからHibernateの依存関係を導入します。

    <dependency>
        <groupId>org.hibernate.orm</groupId>
        <artifactId>hibernate-core</artifactId>
        <version>6.0.0.CR2</version>
    </dependency>
    

    変更が完了すると、サンプル アプリケーションと同じ依存関係を持つ空のSpring Bootアプリケーションを取得できます。

ステップ 5: アプリケーションを実行する

このステップでは、アプリケーション コードをコンパイルして実行し、Web アプリケーションを作成します。 Hibernate はtestデータベース内にplayer_jpaテーブルを作成します。アプリケーションの RESTful API を使用してリクエストを行う場合、これらのリクエストは TiDB クラスターで実行されデータベース トランザクション

このアプリケーションのコードについて詳しく知りたい場合は、 実装の詳細を参照してください。

ステップ 5.1 パラメータの変更

ローカル以外のデフォルト クラスター、 TiDB Cloudクラスター、またはリモート クラスターを使用する場合は、 application.yml ( src/main/resourcesにあります) のspring.datasource.urlspring.datasource.usernamespring.datasource.passwordパラメーターを変更します。

spring:
  datasource:
    url: jdbc:mysql://localhost:4000/test
    username: root
    #    password: xxx
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true
    database-platform: org.hibernate.dialect.TiDBDialect
    hibernate:
      ddl-auto: create-drop

パスワードを123456に設定すると、 TiDB Cloudで得られる接続文字列は次のようになります。

mysql --connect-timeout 15 -u root -h xxx.tidbcloud.com -P 4000 -p

したがって、パラメータは次のように設定する必要があります。

spring:
  datasource:
    url: jdbc:mysql://xxx.tidbcloud.com:4000/test
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true
    database-platform: org.hibernate.dialect.TiDBDialect
    hibernate:
      ddl-auto: create-drop

ステップ 5.2 実行

ターミナル セッションを開き、 spring-jpa-hibernateディレクトリにいることを確認します。このディレクトリにまだ移動していない場合は、次のコマンドを使用してディレクトリに移動します。

cd <path>/tidb-example-java/spring-jpa-hibernate
make

手動でビルドして実行する

手動でビルドする場合は、次の手順に従います。

  1. キャッシュとパッケージをクリアします。

    mvn clean package
    
  2. JAR ファイルを使用してアプリケーションを実行します。

    java -jar target/spring-jpa-hibernate-0.0.1.jar
    

ステップ 5.3 出力

出力の最終部分は次のようになります。

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::             (v3.0.0-M1)

2022-03-28 18:46:01.429  INFO 14923 --- [           main] com.pingcap.App                          : Starting App v0.0.1 using Java 17.0.2 on CheesedeMacBook-Pro.local with PID 14923 (/path/code/tidb-example-java/spring-jpa-hibernate/target/spring-jpa-hibernate-0.0.1.jar started by cheese in /path/code/tidb-example-java/spring-jpa-hibernate)
2022-03-28 18:46:01.430  INFO 14923 --- [           main] com.pingcap.App                          : No active profile set, falling back to default profiles: default
2022-03-28 18:46:01.709  INFO 14923 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2022-03-28 18:46:01.733  INFO 14923 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 20 ms. Found 1 JPA repository interfaces.
2022-03-28 18:46:02.010  INFO 14923 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-03-28 18:46:02.016  INFO 14923 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-03-28 18:46:02.016  INFO 14923 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/10.0.16]
2022-03-28 18:46:02.050  INFO 14923 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-03-28 18:46:02.051  INFO 14923 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 598 ms
2022-03-28 18:46:02.143  INFO 14923 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2022-03-28 18:46:02.173  INFO 14923 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.0.0.CR2
2022-03-28 18:46:02.262  WARN 14923 --- [           main] org.hibernate.orm.deprecation            : HHH90000021: Encountered deprecated setting [javax.persistence.sharedCache.mode], use [jakarta.persistence.sharedCache.mode] instead
2022-03-28 18:46:02.324  INFO 14923 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2022-03-28 18:46:02.415  INFO 14923 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@2575f671
2022-03-28 18:46:02.416  INFO 14923 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2022-03-28 18:46:02.443  INFO 14923 --- [           main] SQL dialect                              : HHH000400: Using dialect: org.hibernate.dialect.TiDBDialect
Hibernate: drop table if exists player_jpa
Hibernate: drop sequence player_jpa_id_seq
Hibernate: create sequence player_jpa_id_seq start with 1 increment by 1
Hibernate: create table player_jpa (id bigint not null, coins integer, goods integer, primary key (id)) engine=InnoDB
2022-03-28 18:46:02.883  INFO 14923 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2022-03-28 18:46:02.888  INFO 14923 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2022-03-28 18:46:03.125  WARN 14923 --- [           main] org.hibernate.orm.deprecation            : HHH90000021: Encountered deprecated setting [javax.persistence.lock.timeout], use [jakarta.persistence.lock.timeout] instead
2022-03-28 18:46:03.132  WARN 14923 --- [           main] org.hibernate.orm.deprecation            : HHH90000021: Encountered deprecated setting [javax.persistence.lock.timeout], use [jakarta.persistence.lock.timeout] instead
2022-03-28 18:46:03.168  WARN 14923 --- [           main] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2022-03-28 18:46:03.307  INFO 14923 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-03-28 18:46:03.311  INFO 14923 --- [           main] com.pingcap.App                          : Started App in 2.072 seconds (JVM running for 2.272)

出力ログは、起動中のアプリケーションの動作を示します。この例では、アプリケーションはトムキャットを使用してサーブレットを開始し、ORM として Hibernate を使用し、データベース接続プールの実装として光CPを使用し、データベースの方言としてorg.hibernate.dialect.TiDBDialectを使用します。起動後、Hibernate はplayer_jpaテーブルとplayer_jpa_id_seqシーケンスを削除して再作成します。起動の最後に、アプリケーションはポート8080でリッスンし、HTTP サービスを外部に提供します。

このアプリケーションのコードについて詳しく知りたい場合は、 実装の詳細を参照してください。

ステップ 6: HTTP リクエスト

サービスが起動して実行されたら、HTTP 要求をバックエンド アプリケーションに送信できます。 http://localhost:8080は、サービスを提供するベース URL です。このチュートリアルでは、一連の HTTP 要求を使用して、サービスの使用方法を示します。

この構成ファイルをローカルにダウンロードして、次に示すように郵便屋さんにインポートできます。

import the collection into Postman

プレーヤーを作成する

[作成] タブをクリックし、[送信] ボタンをクリックして、POST 要求をhttp://localhost:8080/player/に送信します。戻り値は追加されたプレーヤーの数で、1 であると予想されます。

Postman-Create a player

IDでプレイヤー情報を取得する

GetByIDタブをクリックし、[送信] ボタンをクリックして、GET 要求をhttp://localhost:8080/player/1に送信します。戻り値は ID 1のプレイヤーの情報です。

Postman-GetByID

制限付きでプレイヤー情報を一括取得

GetByLimitタブをクリックし、 Sendボタンをクリックして、GET リクエストをhttp://localhost:8080/player/limit/3に送信します。戻り値は、最大 3 人のプレイヤーの情報のリストです。

Postman-GetByLimit

ページごとにプレイヤー情報を取得する

GetByPageタブをクリックし、[送信] ボタンをクリックして、GET 要求をhttp://localhost:8080/player/page?index=0&size=2に送信します。戻り値はインデックス0のページで、1 ページあたり2人のプレイヤーがいます。戻り値には、オフセット、totalPages、および並べ替えなどのページング情報も含まれています。

Postman-GetByPage

プレイヤーを数える

[カウント] タブと [送信] ボタンをクリックして、GET 要求をhttp://localhost:8080/player/countに送信します。戻り値はプレイヤー数です。

Postman-Count

プレイヤーの取引

[取引] タブをクリックし、[送信] ボタンをクリックして、PUT リクエストをhttp://localhost:8080/player/tradeに送信します。要求パラメータは、売り手の ID sellID 、買い手の ID buyID 、購入された商品の数amount 、購入のために消費されたコインの数priceです。

戻り値は、トランザクションが成功したかどうかです。売り手にとって不十分な商品、買い手にとって不十分なコイン、またはデータベース エラーがある場合、 データベース トランザクションは取引が成功せず、プレイヤーのコインや商品が失われないことを保証します。

Postman-Trade

ステップ 6.2 curl リクエストの使用

curl を使用して直接リクエストを行うこともできます。

プレーヤーを作成する

プレーヤーを作成するには、 POSTリクエストを/playerエンドポイントに送信します。例えば:

curl --location --request POST 'http://localhost:8080/player/' --header 'Content-Type: application/json' --data-raw '[{"coins":100,"goods":20}]'

リクエストは JSON をペイロードとして使用します。上記の例は、100 coinsと 20 goodsでプレーヤーを作成することを示しています。戻り値は、作成されたプレーヤーの数です。

1

IDでプレイヤー情報を取得する

プレイヤー情報を取得するには、 GETリクエストを/playerエンドポイントに送信します。次のように、パス パラメーターでプレーヤーのidを指定する必要があります: /player/{id} 。次の例は、 id 1 のプレーヤーの情報を取得する方法を示しています。

curl --location --request GET 'http://localhost:8080/player/1'

戻り値はプレイヤーの情報です:

{
  "coins": 200,
  "goods": 10,
  "id": 1
}

制限付きでプレイヤー情報を一括取得

プレイヤー情報を一括で取得するには、 GETリクエストを/player/limitエンドポイントに送信します。次のように、パス パラメーターでプレイヤーの総数を指定する必要があります: /player/limit/{limit} 。次の例は、最大 3 人のプレーヤーの情報を取得する方法を示しています。

curl --location --request GET 'http://localhost:8080/player/limit/3'

戻り値はプレイヤー情報のリストです:

[
  {
    "coins": 200,
    "goods": 10,
    "id": 1
  },
  {
    "coins": 0,
    "goods": 30,
    "id": 2
  },
  {
    "coins": 100,
    "goods": 20,
    "id": 3
  }
]

ページごとにプレイヤー情報を取得する

ページ分割されたプレイヤー情報を取得するには、 GETリクエストを/player/pageエンドポイントに送信します。追加のパラメーターを指定するには、URL パラメーターを使用する必要があります。次の例は、 indexが 0 のページから情報を取得する方法を示しています。各ページには最大size人のプレイヤーが 2 人います。

curl --location --request GET 'http://localhost:8080/player/page?index=0&size=2'

戻り値はindex 0 のページで、1 ページに 2 人のプレイヤーがリストされています。さらに、戻り値には、オフセット、総ページ数、結果がソートされているかどうかなどのページ付け情報が含まれます。

{
  "content": [
    {
      "coins": 200,
      "goods": 10,
      "id": 1
    },
    {
      "coins": 0,
      "goods": 30,
      "id": 2
    }
  ],
  "empty": false,
  "first": true,
  "last": false,
  "number": 0,
  "numberOfElements": 2,
  "pageable": {
    "offset": 0,
    "pageNumber": 0,
    "pageSize": 2,
    "paged": true,
    "sort": {
      "empty": true,
      "sorted": false,
      "unsorted": true
    },
    "unpaged": false
  },
  "size": 2,
  "sort": {
    "empty": true,
    "sorted": false,
    "unsorted": true
  },
  "totalElements": 4,
  "totalPages": 2
}

プレイヤーを数える

プレーヤーの数を取得するには、 GETリクエストを/player/countエンドポイントに送信します。

curl --location --request GET 'http://localhost:8080/player/count'

戻り値はプレイヤーの数です:

4

プレイヤーの取引

プレイヤー間のトランザクションを開始するには、 PUTリクエストを/player/tradeエンドポイントに送信します。例えば:

curl --location --request PUT 'http://localhost:8080/player/trade' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'sellID=1' \
  --data-urlencode 'buyID=2' \
  --data-urlencode 'amount=10' \
  --data-urlencode 'price=100'

リクエストはフォーム データをペイロードとして使用します。例のリクエストは、売り手の ID ( sellID ) が 1 、買い手の ID ( buyID ) が 2 、購入された商品の数 ( amount ) が 10 、購入のために消費されたコインの数 ( price ) が 100 であることを示しています。

戻り値は、トランザクションが成功したかどうかです。売り手にとって不十分な商品、買い手にとって不十分なコイン、またはデータベース エラーがある場合、 データベース トランザクションは取引が成功せず、プレイヤーのコインや商品が失われないことを保証します。

true

ステップ 6.3 シェル スクリプトを使用したリクエスト

テスト目的でこのシェルスクリプトをダウンロードできます。スクリプトは次の操作を実行します。

  1. ループで 10 人のプレイヤーを作成します。
  2. id of 1 でプレイヤーの情報を取得します。
  3. 最大 3 人のプレイヤーのリストを取得します。
  4. index of 0 とsize of 2 のプレーヤーのページを取得します。
  5. プレイヤーの総数を取得します。
  6. id of 1 のプレイヤーが売り手、 id of 2 のプレイヤーが買い手の取引を行い、10 goodsを 100 coinsで購入します。

このスクリプトはmake requestまたは./request.shで実行できます。結果は次のようになります。

cheese@CheesedeMacBook-Pro spring-jpa-hibernate % make request
./request.sh
loop to create 10 players:
1111111111

get player 1:
{"id":1,"coins":200,"goods":10}

get players by limit 3:
[{"id":1,"coins":200,"goods":10},{"id":2,"coins":0,"goods":30},{"id":3,"coins":100,"goods":20}]

get first players:
{"content":[{"id":1,"coins":200,"goods":10},{"id":2,"coins":0,"goods":30}],"pageable":{"sort":{"empty":true,"unsorted":true,"sorted":false},"offset":0,"pageNumber":0,"pageSize":2,"paged":true,"unpaged":false},"last":false,"totalPages":7,"totalElements":14,"first":true,"size":2,"number":0,"sort":{"empty":true,"unsorted":true,"sorted":false},"numberOfElements":2,"empty":false}

get players count:
14

trade by two players:
false

実装の詳細

このサブセクションでは、サンプル アプリケーション プロジェクトのコンポーネントについて説明します。

概要

このサンプル プロジェクトのカタログ ツリーを以下に示します (一部のわかりにくい部分は削除されています)。

.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── pingcap
        │           ├── App.java
        │           ├── controller
        │           │   └── PlayerController.java
        │           ├── dao
        │           │   ├── PlayerBean.java
        │           │   └── PlayerRepository.java
        │           └── service
        │               ├── PlayerService.java
        │               └── impl
        │                   └── PlayerServiceImpl.java
        └── resources
            └── application.yml
  • pom.xmlは、依存関係やパッケージ化など、プロジェクトの Maven 構成を宣言します。
  • application.ymlは、使用するデータベース アドレス、パスワード、データベースの方言など、プロジェクトのユーザー構成を宣言します。
  • App.javaはプロジェクトのエントリ ポイントです。
  • controllerは、HTTP インターフェイスを外部に公開するパッケージです。
  • serviceは、プロジェクトのインターフェイスとロジックを実装するパッケージです。
  • daoは、データベースへの接続とデータの永続性を実装するパッケージです。

Configuration / コンフィグレーション

この部分では、 pom.xmlファイルの Maven 構成とapplication.ymlファイルのユーザー構成について簡単に説明します。

Maven 構成

pom.xmlのファイルは、プロジェクトの Maven 依存関係、パッケージ化方法、およびパッケージ化情報を宣言する Maven 構成ファイルです。この構成ファイルを生成するプロセスを同じ依存関係を持つ空のアプリケーションを作成する複製するか、プロジェクトに直接コピーすることができます。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>3.0.0-M1</version>
      <relativePath/> <!-- lookup parent from repository -->
   </parent>

   <groupId>com.pingcap</groupId>
   <artifactId>spring-jpa-hibernate</artifactId>
   <version>0.0.1</version>
   <name>spring-jpa-hibernate</name>
   <description>an example for spring boot, jpa, hibernate and TiDB</description>

   <properties>
      <java.version>17</java.version>
      <maven.compiler.source>17</maven.compiler.source>
      <maven.compiler.target>17</maven.compiler.target>
   </properties>

   <dependencies>
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-jpa</artifactId>
         <exclusions>
            <exclusion>
               <groupId>org.hibernate</groupId>
               <artifactId>hibernate-core-jakarta</artifactId>
            </exclusion>
         </exclusions>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-web</artifactId>
      </dependency>

      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
         <scope>runtime</scope>
      </dependency>

      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-test</artifactId>
         <scope>test</scope>
      </dependency>

      <dependency>
         <groupId>org.hibernate.orm</groupId>
         <artifactId>hibernate-core</artifactId>
         <version>6.0.0.CR2</version>
      </dependency>
   </dependencies>

   <build>
      <plugins>
         <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
         </plugin>
      </plugins>
   </build>

   <repositories>
      <repository>
         <id>spring-milestones</id>
         <name>Spring Milestones</name>
         <url>https://repo.spring.io/milestone</url>
         <snapshots>
            <enabled>false</enabled>
         </snapshots>
      </repository>
   </repositories>
   <pluginRepositories>
      <pluginRepository>
         <id>spring-milestones</id>
         <name>Spring Milestones</name>
         <url>https://repo.spring.io/milestone</url>
         <snapshots>
            <enabled>false</enabled>
         </snapshots>
      </pluginRepository>
   </pluginRepositories>
</project>

ユーザー構成

application.ymlの構成ファイルは、データベース アドレス、パスワード、使用するデータベースの方言などのユーザー構成を宣言します。

spring:
  datasource:
    url: jdbc:mysql://localhost:4000/test
    username: root
    #    password: xxx
    driver-class-name: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: true
    database-platform: org.hibernate.dialect.TiDBDialect
    hibernate:
      ddl-auto: create-drop

設定はYAMLに書かれています。フィールドの説明は次のとおりです。

  • spring.datasource.url : データベース接続の URL。
  • spring.datasource.username : データベースのユーザー名。
  • spring.datasource.password : データベースのパスワード。空の。このフィールドをコメントアウトするか削除する必要があります。
  • spring.datasource.driver-class-name : データベース ドライバー。 TiDB は MySQL と互換性があるため、mysql-connector-java ドライバー クラスcom.mysql.cj.jdbcを使用します。
  • jpa.show-sql : このフィールドがtrueに設定されている場合、JPA によって実行される SQL ステートメントが出力されます。
  • jpa.database-platform : 選択されたデータベースの方言。アプリケーションは TiDB に接続するため、 TiDB 方言を選択します。このダイアレクトは Hibernate 6.0.0.Beta2以降のバージョンでのみ使用できることに注意してください。そのため、該当する依存バージョンを選択してください。
  • jpa.hibernate.ddl-auto : create-dropは、プログラムの開始時にテーブルを作成し、終了時にテーブルを削除します。本番環境ではこのオプションを設定しないでください。これはサンプル アプリケーションであるため、このオプションはデータベース データへの影響を最小限に抑えるように設定されています。

エントリーポイント

App.javaファイルはエントリ ポイントです。

package com.pingcap;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.ApplicationPidFileWriter;

@SpringBootApplication
public class App {
   public static void main(String[] args) {
      SpringApplication springApplication = new SpringApplication(App.class);
      springApplication.addListeners(new ApplicationPidFileWriter("spring-jpa-hibernate.pid"));
      springApplication.run(args);
   }
}

エントリ クラスは、Spring Boot アプリケーションの標準構成アノテーション@SpringBootApplicationで始まります。詳細については、Spring Boot 公式ドキュメントの@SpringBootApplicationアノテーションの使用を参照してください。次に、プログラムはApplicationPidFileWriterを使用して、アプリケーションの起動時にspring-jpa-hibernate.pidという名前の PID (プロセス識別番号) ファイルを書き込みます。 PID ファイルを使用して、外部ソースからこのアプリケーションを閉じることができます。

データ アクセス オブジェクト

dao (データ アクセス オブジェクト) パッケージは、データ オブジェクトの永続性を実装します。

エンティティ オブジェクト

PlayerBean.javaのファイルは、データベース内のテーブルに対応するエンティティ オブジェクトです。

package com.pingcap.dao;

import jakarta.persistence.*;

/**
 * it's core entity in hibernate
 * @Table appoint to table name
 */
@Entity
@Table(name = "player_jpa")
public class PlayerBean {
    /**
     * @ID primary key
     * @GeneratedValue generated way. this field will use generator named "player_id"
     * @SequenceGenerator using `sequence` feature to create a generator,
     *    and it named "player_jpa_id_seq" in database, initial form 1 (by `initialValue`
     *    parameter default), and every operator will increase 1 (by `allocationSize`)
     */
    @Id
    @GeneratedValue(generator="player_id")
    @SequenceGenerator(name="player_id", sequenceName="player_jpa_id_seq", allocationSize=1)
    private Long id;

    /**
     * @Column field
     */
    @Column(name = "coins")
    private Integer coins;
    @Column(name = "goods")
    private Integer goods;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Integer getCoins() {
        return coins;
    }

    public void setCoins(Integer coins) {
        this.coins = coins;
    }

    public Integer getGoods() {
        return goods;
    }

    public void setGoods(Integer goods) {
        this.goods = goods;
    }
}

エンティティ クラスには、エンティティ クラスをテーブルにバインドするための追加情報を Hibernate に与えるいくつかの注釈があります。

  • @Entityは、 PlayerBeanがエンティティ クラスであることを宣言します。
  • @Tableは、アノテーション属性nameを使用して、このエンティティ クラスをplayer_jpaテーブルに関連付けます。
  • @Idは、このプロパティがテーブルの主キー列に関連していることを宣言します。
  • @GeneratedValueは、この列の値が自動的に生成され、手動で設定する必要がないことを示します。属性generatorは、ジェネレーターの名前をplayer_idとして指定するために使用されます。
  • @SequenceGenerator順序を使用するジェネレーターを宣言し、アノテーション属性nameを使用してジェネレーターの名前をplayer_idとして宣言します ( @GeneratedValueで指定された名前と一致します)。注釈属性sequenceNameは、データベース内のシーケンスの名前を指定するために使用されます。最後に、注釈属性allocationSizeを使用して、シーケンスのステップ サイズが 1 であることを宣言します。
  • @Columnは、各プライベート属性をplayer_jpaテーブルの列として宣言し、注釈属性nameを使用して、属性に対応する列の名前を決定します。

リポジトリ

データベースレイヤーを抽象化するために、Spring アプリケーションはRepositoryインターフェース、またはRepositoryのサブインターフェースを使用します。このインターフェイスは、テーブルなどのデータベース オブジェクトにマップされます。 JPA は、primay キーを使用してINSERTSELECTなどのいくつかの事前構築済みメソッドを実装します。

package com.pingcap.dao;

import jakarta.persistence.LockModeType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface PlayerRepository extends JpaRepository<PlayerBean, Long> {
    /**
     * use HQL to query by page
     * @param pageable a pageable parameter required by hibernate
     * @return player list package by page message
     */
    @Query(value = "SELECT player_jpa FROM PlayerBean player_jpa")
    Page<PlayerBean> getPlayersByPage(Pageable pageable);

    /**
     * use SQL to query by limit, using named parameter
     * @param limit sql parameter
     * @return player list (max size by limit)
     */
    @Query(value = "SELECT * FROM player_jpa LIMIT :limit", nativeQuery = true)
    List<PlayerBean> getPlayersByLimit(@Param("limit") Integer limit);

    /**
     * query player and add a lock for update
     * @param id player id
     * @return player
     */
    @Lock(value = LockModeType.PESSIMISTIC_WRITE)
    @Query(value = "SELECT player FROM PlayerBean player WHERE player.id = :id")
    // @Query(value = "SELECT * FROM player_jpa WHERE id = :id FOR UPDATE", nativeQuery = true)
    PlayerBean getPlayerAndLock(@Param("id") Long id);
}

PlayerRepositoryインターフェースは、Spring が JPA データ アクセスに使用するJpaRepositoryインターフェースを拡張します。 @Queryアノテーションは、このインターフェースでクエリを実装する方法を Hibernate に伝えるために使用されます。次の 2 つのクエリ構文が使用されます。

  • getPlayersByPageインターフェイスでは、 Hibernate クエリ言語 (HQL) が使用されます。
  • getPlayersByLimitインターフェースでは、ネイティブ SQL が使用されます。インターフェースがネイティブ SQL 構文を使用する場合、 @Query注釈パラメーターnativeQuerytrueに設定する必要があります。

getPlayersByLimit注釈の SQL では、 :limitは Hibernate では名前付きパラメーターと呼ばれます。 Hibernate は、アノテーションが存在するインターフェース内の名前でパラメーターを自動的に見つけてスプライスします。 @Paramを使用して、注入のパラメーターとは異なる名前を指定することもできます。

getPlayerAndLockでは、注釈@Lockを使用して悲観的ロックが適用されることを宣言します。その他のロック方法については、 エンティティのロックを参照してください。 @Lock注釈はHQLと共に使用する必要があります。そうしないと、エラーが発生します。ロックに SQL を直接使用する場合は、コメントの注釈を使用できます。

@Query(value = "SELECT * FROM player_jpa WHERE id = :id FOR UPDATE", nativeQuery = true)

上記の SQL ステートメントでは、 FOR UPDATEを使用してロックを直接追加しています。また、TiDB SELECTステートメントを使用して、原則をさらに深く掘り下げることもできます。

ロジックの実装

ロジック実装レイヤーは、プロジェクトによって実装されるインターフェイスとロジックを含むserviceのパッケージです。

インターフェース

PlayerService.javaファイルは、クラスを直接記述するのではなく、論理インターフェイスを定義し、インターフェイスを実装します。これは、例をできるだけ実際の使用に近づけ、設計の開閉原理を反映するためです。このインターフェースを省略して、実装クラスを依存関係クラスに直接注入することもできますが、この方法はお勧めしません。

package com.pingcap.service;

import com.pingcap.dao.PlayerBean;
import org.springframework.data.domain.Page;

import java.util.List;

public interface PlayerService {
    /**
     * create players by passing in a List of PlayerBean
     *
     * @param players will create players list
     * @return The number of create accounts
     */
    Integer createPlayers(List<PlayerBean> players);

    /**
     * buy goods and transfer funds between one player and another in one transaction
     * @param sellId sell player id
     * @param buyId buy player id
     * @param amount goods amount, if sell player has not enough goods, the trade will break
     * @param price price should pay, if buy player has not enough coins, the trade will break
     */
    void buyGoods(Long sellId, Long buyId, Integer amount, Integer price) throws RuntimeException;

    /**
     * get the player info by id.
     *
     * @param id player id
     * @return the player of this id
     */
    PlayerBean getPlayerByID(Long id);

    /**
     * get a subset of players from the data store by limit.
     *
     * @param limit return max size
     * @return player list
     */
    List<PlayerBean> getPlayers(Integer limit);

    /**
     * get a page of players from the data store.
     *
     * @param index page index
     * @param size page size
     * @return player list
     */
    Page<PlayerBean> getPlayersByPage(Integer index, Integer size);

    /**
     * count players from the data store.
     *
     * @return all players count
     */
    Long countPlayers();
}

実装 (重要)

PlayerService.javaファイルは、すべてのデータ処理ロジックを含むPlayerServiceインターフェイスを実装します。

package com.pingcap.service.impl;

import com.pingcap.dao.PlayerBean;
import com.pingcap.dao.PlayerRepository;
import com.pingcap.service.PlayerService;
import jakarta.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * PlayerServiceImpl implements PlayerService interface
 * @Transactional it means every method in this class, will package by a pair of
 *     transaction.begin() and transaction.commit(). and it will be call
 *     transaction.rollback() when method throw an exception
 */
@Service
@Transactional
public class PlayerServiceImpl implements PlayerService {
    @Autowired
    private PlayerRepository playerRepository;

    @Override
    public Integer createPlayers(List<PlayerBean> players) {
        return playerRepository.saveAll(players).size();
    }

    @Override
    public void buyGoods(Long sellId, Long buyId, Integer amount, Integer price) throws RuntimeException {
        PlayerBean buyPlayer = playerRepository.getPlayerAndLock(buyId);
        PlayerBean sellPlayer = playerRepository.getPlayerAndLock(sellId);
        if (buyPlayer == null || sellPlayer == null) {
            throw new RuntimeException("sell or buy player not exist");
        }

        if (buyPlayer.getCoins() < price || sellPlayer.getGoods() < amount) {
            throw new RuntimeException("coins or goods not enough, rollback");
        }

        buyPlayer.setGoods(buyPlayer.getGoods() + amount);
        buyPlayer.setCoins(buyPlayer.getCoins() - price);
        playerRepository.save(buyPlayer);

        sellPlayer.setGoods(sellPlayer.getGoods() - amount);
        sellPlayer.setCoins(sellPlayer.getCoins() + price);
        playerRepository.save(sellPlayer);
    }

    @Override
    public PlayerBean getPlayerByID(Long id) {
        return playerRepository.findById(id).orElse(null);
    }

    @Override
    public List<PlayerBean> getPlayers(Integer limit) {
        return playerRepository.getPlayersByLimit(limit);
    }

    @Override
    public Page<PlayerBean> getPlayersByPage(Integer index, Integer size) {
        return playerRepository.getPlayersByPage(PageRequest.of(index, size));
    }

    @Override
    public Long countPlayers() {
        return playerRepository.count();
    }
}

@Serviceアノテーションは、このオブジェクトのライフサイクルがSpringによって管理されることを宣言するために使用されます。

PlayerServiceImpl実装クラスには、 @Serviceアノテーションに加えて@Transactionalアノテーションもあります。アプリケーションでトランザクション管理が有効になっている場合 ( @EnableTransactionManagementを使用して有効にできますが、デフォルトではSpring Bootで有効になっています。手動で構成する必要はありません)、 Springは自動的に@Transactionalアノテーションを持つすべてのオブジェクトをプロキシでラップし、オブジェクト呼び出し処理にこのプロキシを使用します。

エージェントが@Transactionalアノテーションを使用してオブジェクト内の関数を呼び出すと、次のように単純に想定できます。

  • 関数の先頭で、 transaction.begin()でトランザクションを開始します。
  • 関数が戻ると、 transaction.commit()を呼び出してトランザクションをコミットします。
  • 実行時エラーが発生すると、エージェントはtransaction.rollback()を呼び出してロールバックします。

取引の詳細についてはデータベース トランザクションを参照するか、 Spring Web サイトのSpring Framework の宣言型トランザクションの実装を理解するを参照してください。

すべての実装クラスで、 buyGoodsの機能に注意が必要です。関数が非論理的な操作に遭遇すると、例外をスローし、Hibernate にトランザクションのロールバックを実行して不正なデータを防ぐように指示します。

外部 HTTP インターフェイス

controllerパッケージは、HTTP インターフェイスを外部に公開し、 残りの API経由でサービスにアクセスできるようにします。

package com.pingcap.controller;

import com.pingcap.dao.PlayerBean;
import com.pingcap.service.PlayerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.lang.NonNull;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/player")
public class PlayerController {
    @Autowired
    private PlayerService playerService;

    @PostMapping
    public Integer createPlayer(@RequestBody @NonNull List<PlayerBean> playerList) {
        return playerService.createPlayers(playerList);
    }

    @GetMapping("/{id}")
    public PlayerBean getPlayerByID(@PathVariable Long id) {
        return playerService.getPlayerByID(id);
    }

    @GetMapping("/limit/{limit_size}")
    public List<PlayerBean> getPlayerByLimit(@PathVariable("limit_size") Integer limit) {
        return playerService.getPlayers(limit);
    }

    @GetMapping("/page")
    public Page<PlayerBean> getPlayerByPage(@RequestParam Integer index, @RequestParam("size") Integer size) {
        return playerService.getPlayersByPage(index, size);
    }

    @GetMapping("/count")
    public Long getPlayersCount() {
        return playerService.countPlayers();
    }

    @PutMapping("/trade")
    public Boolean trade(@RequestParam Long sellID, @RequestParam Long buyID, @RequestParam Integer amount, @RequestParam Integer price) {
        try {
            playerService.buyGoods(sellID, buyID, amount, price);
        } catch (RuntimeException e) {
            return false;
        }

        return true;
    }
}

PlayerControllerは、機能を示すためにできるだけ多くの注釈を使用します。実際のプロジェクトでは、会社やチームのルールに従いながら、一貫したスタイルを維持してください。 PlayerControllerの注釈は次のように説明されています。

  • @RestControllerPlayerControllerウェブコントローラーとして宣言し、戻り値をJSON出力としてシリアル化します。
  • @RequestMappingは URL エンドポイントを/playerにマップします。つまり、このWeb Controller/player URL に送信されたリクエストのみをリッスンします。
  • @Autowiredは、 Springのコンテナーが協力する Bean 間の関係をオートワイヤーできることを意味します。宣言にはPlayerServiceオブジェクトが必要です。これはインターフェイスであり、使用する実装クラスを指定しません。これはSpringによって組み立てられます。このアセンブリのルールについては、Spring の公式 Web サイトのIoC コンテナーを参照してください。
  • @PostMappingは、この関数が HTTP の役職要求に応答することを宣言します。
    • @RequestBodyは、HTTP ペイロード全体がplayerListパラメーターに解析されることを宣言します。
    • @NonNullは、パラメーターが null であってはならないことを宣言します。そうでない場合は、エラーが返されます。
  • @GetMappingは、この関数が HTTP の得る要求に応答することを宣言します。
    • @PathVariableは、アノテーションに{id}{limit_size}などのプレースホルダーがあり、これらは@PathVariableでアノテーションが付けられた変数にバインドされていることを示します。このようなバインディングは、注釈属性nameに基づいています。注釈属性nameが指定されていない場合は、変数名と同じです。変数名は省略できます。つまり、 @PathVariable(name="limit_size")@PathVariable("limit_size")のように記述できます。
  • @PutMappingは、この関数が HTTP の置く要求に応答することを宣言します。
  • @RequestParamは、この関数がリクエスト内の URL パラメーター、フォーム パラメーター、およびその他のパラメーターを解析し、それらを注釈付き変数にバインドすることを宣言します。
エコシステム
TiDB
TiKV
TiFlash
TiSpark
Chaos Mesh
© 2023 PingCAP. All Rights Reserved.