TiDB 和 Java 的简单 CRUD 应用程序

本文档将展示如何使用 TiDB 和 Java 来构造一个简单的 CRUD 应用程序。

Java - 图1

注意

推荐使用 Java 8 及以上版本进行 TiDB 的应用程序的编写。

拓展学习视频

Java - 图2

小贴士

如果你希望使用 Spring Boot 进行 TiDB 应用程序的编写,可以查看 Build the TiDB Application using Spring Boot

第 1 步:启动你的 TiDB 集群

本节将介绍 TiDB 集群的启动方法。

  • TiDB Cloud
  • 本地集群
  • Gitpod

创建 Serverless Tier 集群

你可以部署一个本地测试的 TiDB 集群或正式的 TiDB 集群。详细步骤,请参考:

基于 Git 的预配置的开发环境:现在就试试

该环境会自动克隆代码,并通过 TiUP 部署测试集群。

第 2 步:获取代码

  1. git clone https://github.com/pingcap-inc/tidb-example-java.git
  • 使用 Mybatis(推荐)
  • 使用 Hibernate(推荐)
  • 使用 JDBC

Mybatis 是当前比较流行的开源 Java 应用持久层框架,本文将以 Maven 插件的方式使用 MyBatis Generator 生成部分持久层代码。

进入目录 plain-java-mybatis

  1. cd plain-java-mybatis

目录结构如下所示:

  1. .
  2. ├── Makefile
  3. ├── pom.xml
  4. └── src
  5. └── main
  6. ├── java
  7. └── com
  8. └── pingcap
  9. ├── MybatisExample.java
  10. ├── dao
  11. └── PlayerDAO.java
  12. └── model
  13. ├── Player.java
  14. ├── PlayerMapper.java
  15. └── PlayerMapperEx.java
  16. └── resources
  17. ├── dbinit.sql
  18. ├── log4j.properties
  19. ├── mapper
  20. ├── PlayerMapper.xml
  21. └── PlayerMapperEx.xml
  22. ├── mybatis-config.xml
  23. └── mybatis-generator.xml

其中,自动生成的文件有:

  • src/main/java/com/pingcap/model/Player.java:Player 实体类文件
  • src/main/java/com/pingcap/model/PlayerMapper.java:Player Mapper 的接口文件
  • src/main/resources/mapper/PlayerMapper.xml:Player Mapper 的 XML 映射,它是 Mybatis 用于生成 Player Mapper 接口的实现类的配置

这些文件的生成策略被写在了 mybatis-generator.xml 配置文件内,它是 Mybatis Generator 的配置文件,下面配置文件中添加了使用方法的说明:

  1. <!DOCTYPE generatorConfiguration PUBLIC
  2. "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  3. "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
  4. <generatorConfiguration>
  5. <!--
  6. <context/> entire document: https://mybatis.org/generator/configreference/context.html
  7. context.id: A unique identifier you like
  8. context.targetRuntime: Used to specify the runtime target for generated code.
  9. It has MyBatis3DynamicSql / MyBatis3Kotlin / MyBatis3 / MyBatis3Simple 4 selection to choice.
  10. -->
  11. <context id="simple" targetRuntime="MyBatis3">
  12. <!--
  13. <commentGenerator/> entire document: https://mybatis.org/generator/configreference/commentGenerator.html
  14. commentGenerator:
  15. - property(suppressDate): remove timestamp in comments
  16. - property(suppressAllComments): remove all comments
  17. -->
  18. <commentGenerator>
  19. <property name="suppressDate" value="true"/>
  20. <property name="suppressAllComments" value="true" />
  21. </commentGenerator>
  22. <!--
  23. <jdbcConnection/> entire document: https://mybatis.org/generator/configreference/jdbcConnection.html
  24. jdbcConnection.driverClass: The fully qualified class name for the JDBC driver used to access the database.
  25. Used mysql-connector-java:5.1.49, should specify JDBC is com.mysql.jdbc.Driver
  26. jdbcConnection.connectionURL: The JDBC connection URL used to access the database.
  27. -->
  28. <jdbcConnection driverClass="com.mysql.jdbc.Driver"
  29. connectionURL="jdbc:mysql://localhost:4000/test?user=root" />
  30. <!--
  31. <javaModelGenerator/> entire document: https://mybatis.org/generator/configreference/javaModelGenerator.html
  32. Model code file will be generated at ${targetProject}/${targetPackage}
  33. javaModelGenerator:
  34. - property(constructorBased): If it's true, generator will create constructor function in model
  35. -->
  36. <javaModelGenerator targetPackage="com.pingcap.model" targetProject="src/main/java">
  37. <property name="constructorBased" value="true"/>
  38. </javaModelGenerator>
  39. <!--
  40. <sqlMapGenerator/> entire document: https://mybatis.org/generator/configreference/sqlMapGenerator.html
  41. XML SQL mapper file will be generated at ${targetProject}/${targetPackage}
  42. -->
  43. <sqlMapGenerator targetPackage="." targetProject="src/main/resources/mapper"/>
  44. <!--
  45. <javaClientGenerator/> entire document: https://mybatis.org/generator/configreference/javaClientGenerator.html
  46. Java code mapper interface file will be generated at ${targetProject}/${targetPackage}
  47. javaClientGenerator.type (context.targetRuntime is MyBatis3):
  48. This attribute indicated Mybatis how to implement interface.
  49. It has ANNOTATEDMAPPER / MIXEDMAPPER / XMLMAPPER 3 selection to choice.
  50. -->
  51. <javaClientGenerator type="XMLMAPPER" targetPackage="com.pingcap.model" targetProject="src/main/java"/>
  52. <!--
  53. <table/> entire document: https://mybatis.org/generator/configreference/table.html
  54. table.tableName: The name of the database table.
  55. table.domainObjectName: The base name from which generated object names will be generated. If not specified, MBG will generate a name automatically based on the tableName.
  56. table.enableCountByExample: Signifies whether a count by example statement should be generated.
  57. table.enableUpdateByExample: Signifies whether an update by example statement should be generated.
  58. table.enableDeleteByExample: Signifies whether a delete by example statement should be generated.
  59. table.enableSelectByExample: Signifies whether a select by example statement should be generated.
  60. table.selectByExampleQueryId: This value will be added to the select list of the select by example statement in this form: "'<value>' as QUERYID".
  61. -->
  62. <table tableName="player" domainObjectName="Player"
  63. enableCountByExample="false" enableUpdateByExample="false"
  64. enableDeleteByExample="false" enableSelectByExample="false"
  65. selectByExampleQueryId="false"/>
  66. </context>
  67. </generatorConfiguration>

mybatis-generator.xmlpom.xml 中,以 mybatis-generator-maven-plugin 插件配置的方式被引入:

  1. <plugin>
  2. <groupId>org.mybatis.generator</groupId>
  3. <artifactId>mybatis-generator-maven-plugin</artifactId>
  4. <version>1.4.1</version>
  5. <configuration>
  6. <configurationFile>src/main/resources/mybatis-generator.xml</configurationFile>
  7. <verbose>true</verbose>
  8. <overwrite>true</overwrite>
  9. </configuration>
  10. <dependencies>
  11. <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
  12. <dependency>
  13. <groupId>mysql</groupId>
  14. <artifactId>mysql-connector-java</artifactId>
  15. <version>5.1.49</version>
  16. </dependency>
  17. </dependencies>
  18. </plugin>

在 Maven 插件内引入后,可删除旧的生成文件后,通过命令 mvn mybatis-generate 生成新的文件。或者你也可以使用已经编写好的 make 命令,通过 make gen 来同时删除旧文件,并生成新文件。

Java - 图3

注意

mybatis-generator.xml 中的属性 configuration.overwrite 仅可控制新生成的 Java 代码文件使用覆盖方式被写入,但 XML 映射文件仍会以追加方式写入。因此,推荐在 Mybaits Generator 生成新的文件前,先删除掉旧的文件。

Player.java 是使用 Mybatis Generator 生成出的数据实体类文件,为数据库表在程序内的映射。Player 类的每个属性都对应着 player 表的一个字段。

  1. package com.pingcap.model;
  2. public class Player {
  3. private String id;
  4. private Integer coins;
  5. private Integer goods;
  6. public Player(String id, Integer coins, Integer goods) {
  7. this.id = id;
  8. this.coins = coins;
  9. this.goods = goods;
  10. }
  11. public Player() {
  12. super();
  13. }
  14. public String getId() {
  15. return id;
  16. }
  17. public void setId(String id) {
  18. this.id = id;
  19. }
  20. public Integer getCoins() {
  21. return coins;
  22. }
  23. public void setCoins(Integer coins) {
  24. this.coins = coins;
  25. }
  26. public Integer getGoods() {
  27. return goods;
  28. }
  29. public void setGoods(Integer goods) {
  30. this.goods = goods;
  31. }
  32. }

PlayerMapper.java 是使用 Mybatis Generator 生成出的映射接口文件,它仅规定了接口,接口的实现类是由 Mybatis 来通过 XML 或注解自动生成的:

  1. package com.pingcap.model;
  2. import com.pingcap.model.Player;
  3. public interface PlayerMapper {
  4. int deleteByPrimaryKey(String id);
  5. int insert(Player row);
  6. int insertSelective(Player row);
  7. Player selectByPrimaryKey(String id);
  8. int updateByPrimaryKeySelective(Player row);
  9. int updateByPrimaryKey(Player row);
  10. }

PlayerMapper.xml 是使用 Mybatis Generator 生成出的映射 XML 文件,Mybatis 将使用这个文件自动生成 PlayerMapper 接口的实现类:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.pingcap.model.PlayerMapper">
  4. <resultMap id="BaseResultMap" type="com.pingcap.model.Player">
  5. <constructor>
  6. <idArg column="id" javaType="java.lang.String" jdbcType="VARCHAR" />
  7. <arg column="coins" javaType="java.lang.Integer" jdbcType="INTEGER" />
  8. <arg column="goods" javaType="java.lang.Integer" jdbcType="INTEGER" />
  9. </constructor>
  10. </resultMap>
  11. <sql id="Base_Column_List">
  12. id, coins, goods
  13. </sql>
  14. <select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="BaseResultMap">
  15. select
  16. <include refid="Base_Column_List" />
  17. from player
  18. where id = #{id,jdbcType=VARCHAR}
  19. </select>
  20. <delete id="deleteByPrimaryKey" parameterType="java.lang.String">
  21. delete from player
  22. where id = #{id,jdbcType=VARCHAR}
  23. </delete>
  24. <insert id="insert" parameterType="com.pingcap.model.Player">
  25. insert into player (id, coins, goods
  26. )
  27. values (#{id,jdbcType=VARCHAR}, #{coins,jdbcType=INTEGER}, #{goods,jdbcType=INTEGER}
  28. )
  29. </insert>
  30. <insert id="insertSelective" parameterType="com.pingcap.model.Player">
  31. insert into player
  32. <trim prefix="(" suffix=")" suffixOverrides=",">
  33. <if test="id != null">
  34. id,
  35. </if>
  36. <if test="coins != null">
  37. coins,
  38. </if>
  39. <if test="goods != null">
  40. goods,
  41. </if>
  42. </trim>
  43. <trim prefix="values (" suffix=")" suffixOverrides=",">
  44. <if test="id != null">
  45. #{id,jdbcType=VARCHAR},
  46. </if>
  47. <if test="coins != null">
  48. #{coins,jdbcType=INTEGER},
  49. </if>
  50. <if test="goods != null">
  51. #{goods,jdbcType=INTEGER},
  52. </if>
  53. </trim>
  54. </insert>
  55. <update id="updateByPrimaryKeySelective" parameterType="com.pingcap.model.Player">
  56. update player
  57. <set>
  58. <if test="coins != null">
  59. coins = #{coins,jdbcType=INTEGER},
  60. </if>
  61. <if test="goods != null">
  62. goods = #{goods,jdbcType=INTEGER},
  63. </if>
  64. </set>
  65. where id = #{id,jdbcType=VARCHAR}
  66. </update>
  67. <update id="updateByPrimaryKey" parameterType="com.pingcap.model.Player">
  68. update player
  69. set coins = #{coins,jdbcType=INTEGER},
  70. goods = #{goods,jdbcType=INTEGER}
  71. where id = #{id,jdbcType=VARCHAR}
  72. </update>
  73. </mapper>

由于 Mybatis Generator 需要逆向生成源码,因此,数据库中需先行有此表结构,可使用 dbinit.sql 生成表结构:

  1. USE test;
  2. DROP TABLE IF EXISTS player;
  3. CREATE TABLE player (
  4. `id` VARCHAR(36),
  5. `coins` INTEGER,
  6. `goods` INTEGER,
  7. PRIMARY KEY (`id`)
  8. );

额外拆分接口 PlayerMapperEx 继承 PlayerMapper,并且编写与之匹配的 PlayerMapperEx.xml。避免直接更改 PlayerMapper.javaPlayerMapper.xml。这是为了规避 Mybatis Generator 的反复生成,影响到自行编写的代码。

PlayerMapperEx.java 中定义自行增加的接口:

  1. package com.pingcap.model;
  2. import java.util.List;
  3. public interface PlayerMapperEx extends PlayerMapper {
  4. Player selectByPrimaryKeyWithLock(String id);
  5. List<Player> selectByLimit(Integer limit);
  6. Integer count();
  7. }

PlayerMapperEx.xml 中定义映射规则:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  3. <mapper namespace="com.pingcap.model.PlayerMapperEx">
  4. <resultMap id="BaseResultMap" type="com.pingcap.model.Player">
  5. <constructor>
  6. <idArg column="id" javaType="java.lang.String" jdbcType="VARCHAR" />
  7. <arg column="coins" javaType="java.lang.Integer" jdbcType="INTEGER" />
  8. <arg column="goods" javaType="java.lang.Integer" jdbcType="INTEGER" />
  9. </constructor>
  10. </resultMap>
  11. <sql id="Base_Column_List">
  12. id, coins, goods
  13. </sql>
  14. <select id="selectByPrimaryKeyWithLock" parameterType="java.lang.String" resultMap="BaseResultMap">
  15. select
  16. <include refid="Base_Column_List" />
  17. from player
  18. where `id` = #{id,jdbcType=VARCHAR}
  19. for update
  20. </select>
  21. <select id="selectByLimit" parameterType="java.lang.Integer" resultMap="BaseResultMap">
  22. select
  23. <include refid="Base_Column_List" />
  24. from player
  25. limit #{id,jdbcType=INTEGER}
  26. </select>
  27. <select id="count" resultType="java.lang.Integer">
  28. select count(*) from player
  29. </select>
  30. </mapper>

PlayerDAO.java 是程序用来管理数据对象的类。其中 DAOData Access Object 的缩写。在其中定义了一系列数据的操作方法,用于数据的写入。

  1. package com.pingcap.dao;
  2. import com.pingcap.model.Player;
  3. import com.pingcap.model.PlayerMapperEx;
  4. import org.apache.ibatis.session.SqlSession;
  5. import org.apache.ibatis.session.SqlSessionFactory;
  6. import java.util.List;
  7. import java.util.function.Function;
  8. public class PlayerDAO {
  9. public static class NotEnoughException extends RuntimeException {
  10. public NotEnoughException(String message) {
  11. super(message);
  12. }
  13. }
  14. // Run SQL code in a way that automatically handles the
  15. // transaction retry logic, so we don't have to duplicate it in
  16. // various places.
  17. public Object runTransaction(SqlSessionFactory sessionFactory, Function<PlayerMapperEx, Object> fn) {
  18. Object resultObject = null;
  19. SqlSession session = null;
  20. try {
  21. // open a session with autoCommit is false
  22. session = sessionFactory.openSession(false);
  23. // get player mapper
  24. PlayerMapperEx playerMapperEx = session.getMapper(PlayerMapperEx.class);
  25. resultObject = fn.apply(playerMapperEx);
  26. session.commit();
  27. System.out.println("APP: COMMIT;");
  28. } catch (Exception e) {
  29. if (e instanceof NotEnoughException) {
  30. System.out.printf("APP: ROLLBACK BY LOGIC; \n%s\n", e.getMessage());
  31. } else {
  32. System.out.printf("APP: ROLLBACK BY ERROR; \n%s\n", e.getMessage());
  33. }
  34. if (session != null) {
  35. session.rollback();
  36. }
  37. } finally {
  38. if (session != null) {
  39. session.close();
  40. }
  41. }
  42. return resultObject;
  43. }
  44. public Function<PlayerMapperEx, Object> createPlayers(List<Player> players) {
  45. return playerMapperEx -> {
  46. Integer addedPlayerAmount = 0;
  47. for (Player player: players) {
  48. playerMapperEx.insert(player);
  49. addedPlayerAmount ++;
  50. }
  51. System.out.printf("APP: createPlayers() --> %d\n", addedPlayerAmount);
  52. return addedPlayerAmount;
  53. };
  54. }
  55. public Function<PlayerMapperEx, Object> buyGoods(String sellId, String buyId, Integer amount, Integer price) {
  56. return playerMapperEx -> {
  57. Player sellPlayer = playerMapperEx.selectByPrimaryKeyWithLock(sellId);
  58. Player buyPlayer = playerMapperEx.selectByPrimaryKeyWithLock(buyId);
  59. if (buyPlayer == null || sellPlayer == null) {
  60. throw new NotEnoughException("sell or buy player not exist");
  61. }
  62. if (buyPlayer.getCoins() < price || sellPlayer.getGoods() < amount) {
  63. throw new NotEnoughException("coins or goods not enough, rollback");
  64. }
  65. int affectRows = 0;
  66. buyPlayer.setGoods(buyPlayer.getGoods() + amount);
  67. buyPlayer.setCoins(buyPlayer.getCoins() - price);
  68. affectRows += playerMapperEx.updateByPrimaryKey(buyPlayer);
  69. sellPlayer.setGoods(sellPlayer.getGoods() - amount);
  70. sellPlayer.setCoins(sellPlayer.getCoins() + price);
  71. affectRows += playerMapperEx.updateByPrimaryKey(sellPlayer);
  72. System.out.printf("APP: buyGoods --> sell: %s, buy: %s, amount: %d, price: %d\n", sellId, buyId, amount, price);
  73. return affectRows;
  74. };
  75. }
  76. public Function<PlayerMapperEx, Object> getPlayerByID(String id) {
  77. return playerMapperEx -> playerMapperEx.selectByPrimaryKey(id);
  78. }
  79. public Function<PlayerMapperEx, Object> printPlayers(Integer limit) {
  80. return playerMapperEx -> {
  81. List<Player> players = playerMapperEx.selectByLimit(limit);
  82. for (Player player: players) {
  83. System.out.println("\n[printPlayers]:\n" + player);
  84. }
  85. return 0;
  86. };
  87. }
  88. public Function<PlayerMapperEx, Object> countPlayers() {
  89. return PlayerMapperEx::count;
  90. }
  91. }

MybatisExampleplain-java-mybatis 这个示例程序的主类。其中定义了入口函数:

  1. package com.pingcap;
  2. import com.pingcap.dao.PlayerDAO;
  3. import com.pingcap.model.Player;
  4. import org.apache.ibatis.io.Resources;
  5. import org.apache.ibatis.session.SqlSessionFactory;
  6. import org.apache.ibatis.session.SqlSessionFactoryBuilder;
  7. import java.io.IOException;
  8. import java.io.InputStream;
  9. import java.util.Arrays;
  10. import java.util.Collections;
  11. public class MybatisExample {
  12. public static void main( String[] args ) throws IOException {
  13. // 1. Create a SqlSessionFactory based on our mybatis-config.xml configuration
  14. // file, which defines how to connect to the database.
  15. InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
  16. SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
  17. // 2. And then, create DAO to manager your data
  18. PlayerDAO playerDAO = new PlayerDAO();
  19. // 3. Run some simple examples.
  20. // Create a player who has 1 coin and 1 goods.
  21. playerDAO.runTransaction(sessionFactory, playerDAO.createPlayers(
  22. Collections.singletonList(new Player("test", 1, 1))));
  23. // Get a player.
  24. Player testPlayer = (Player)playerDAO.runTransaction(sessionFactory, playerDAO.getPlayerByID("test"));
  25. System.out.printf("PlayerDAO.getPlayer:\n => id: %s\n => coins: %s\n => goods: %s\n",
  26. testPlayer.getId(), testPlayer.getCoins(), testPlayer.getGoods());
  27. // Count players amount.
  28. Integer count = (Integer)playerDAO.runTransaction(sessionFactory, playerDAO.countPlayers());
  29. System.out.printf("PlayerDAO.countPlayers:\n => %d total players\n", count);
  30. // Print 3 players.
  31. playerDAO.runTransaction(sessionFactory, playerDAO.printPlayers(3));
  32. // 4. Getting further.
  33. // Player 1: id is "1", has only 100 coins.
  34. // Player 2: id is "2", has 114514 coins, and 20 goods.
  35. Player player1 = new Player("1", 100, 0);
  36. Player player2 = new Player("2", 114514, 20);
  37. // Create two players "by hand", using the INSERT statement on the backend.
  38. int addedCount = (Integer)playerDAO.runTransaction(sessionFactory,
  39. playerDAO.createPlayers(Arrays.asList(player1, player2)));
  40. System.out.printf("PlayerDAO.createPlayers:\n => %d total inserted players\n", addedCount);
  41. // Player 1 wants to buy 10 goods from player 2.
  42. // It will cost 500 coins, but player 1 cannot afford it.
  43. System.out.println("\nPlayerDAO.buyGoods:\n => this trade will fail");
  44. Integer updatedCount = (Integer)playerDAO.runTransaction(sessionFactory,
  45. playerDAO.buyGoods(player2.getId(), player1.getId(), 10, 500));
  46. System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount);
  47. // So player 1 has to reduce the incoming quantity to two.
  48. System.out.println("\nPlayerDAO.buyGoods:\n => this trade will success");
  49. updatedCount = (Integer)playerDAO.runTransaction(sessionFactory,
  50. playerDAO.buyGoods(player2.getId(), player1.getId(), 2, 100));
  51. System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount);
  52. }
  53. }

当前开源比较流行的 Java ORM 为 Hibernate,且 Hibernate 在版本 6.0.0.Beta2 及以后支持了 TiDB 方言。完美适配了 TiDB 的特性。因此,此处将以 6.0.0.Beta2 + 版本进行说明。

进入目录 plain-java-hibernate

  1. cd plain-java-hibernate

目录结构如下所示:

  1. .
  2. ├── Makefile
  3. ├── plain-java-hibernate.iml
  4. ├── pom.xml
  5. └── src
  6. └── main
  7. ├── java
  8. └── com
  9. └── pingcap
  10. └── HibernateExample.java
  11. └── resources
  12. └── hibernate.cfg.xml

其中,hibernate.cfg.xml 为 Hibernate 配置文件,定义了:

  1. <?xml version='1.0' encoding='utf-8'?>
  2. <!DOCTYPE hibernate-configuration PUBLIC
  3. "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  4. "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
  5. <hibernate-configuration>
  6. <session-factory>
  7. <!-- Database connection settings -->
  8. <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
  9. <property name="hibernate.dialect">org.hibernate.dialect.TiDBDialect</property>
  10. <property name="hibernate.connection.url">jdbc:mysql://localhost:4000/test</property>
  11. <property name="hibernate.connection.username">root</property>
  12. <property name="hibernate.connection.password"></property>
  13. <property name="hibernate.connection.autocommit">false</property>
  14. <!-- Required so a table can be created from the 'PlayerDAO' class -->
  15. <property name="hibernate.hbm2ddl.auto">create-drop</property>
  16. <!-- Optional: Show SQL output for debugging -->
  17. <property name="hibernate.show_sql">true</property>
  18. <property name="hibernate.format_sql">true</property>
  19. </session-factory>
  20. </hibernate-configuration>

HibernateExample.javaplain-java-hibernate 这个示例程序的主体。使用 Hibernate 时,相较于 JDBC,这里仅需写入配置文件地址,Hibernate 屏蔽了创建数据库连接时,不同数据库差异的细节。

PlayerDAO 是程序用来管理数据对象的类。其中 DAOData Access Object 的缩写。其中定义了一系列数据的操作方法,用来提供数据的写入能力。相较于 JDBC,Hibernate 封装了大量的操作,如对象映射、基本对象的 CRUD 等,极大的简化了代码量。

PlayerBean 是数据实体类,为数据库表在程序内的映射。PlayerBean 的每个属性都对应着 player 表的一个字段。相较于 JDBC,Hibernate 的 PlayerBean 实体类为了给 Hibernate 提供更多的信息,加入了注解,用来指示映射关系。

  1. package com.pingcap;
  2. import jakarta.persistence.Column;
  3. import jakarta.persistence.Entity;
  4. import jakarta.persistence.Id;
  5. import jakarta.persistence.Table;
  6. import org.hibernate.JDBCException;
  7. import org.hibernate.Session;
  8. import org.hibernate.SessionFactory;
  9. import org.hibernate.Transaction;
  10. import org.hibernate.cfg.Configuration;
  11. import org.hibernate.query.NativeQuery;
  12. import org.hibernate.query.Query;
  13. import java.util.Arrays;
  14. import java.util.Collections;
  15. import java.util.List;
  16. import java.util.function.Function;
  17. @Entity
  18. @Table(name = "player_hibernate")
  19. class PlayerBean {
  20. @Id
  21. private String id;
  22. @Column(name = "coins")
  23. private Integer coins;
  24. @Column(name = "goods")
  25. private Integer goods;
  26. public PlayerBean() {
  27. }
  28. public PlayerBean(String id, Integer coins, Integer goods) {
  29. this.id = id;
  30. this.coins = coins;
  31. this.goods = goods;
  32. }
  33. public String getId() {
  34. return id;
  35. }
  36. public void setId(String id) {
  37. this.id = id;
  38. }
  39. public Integer getCoins() {
  40. return coins;
  41. }
  42. public void setCoins(Integer coins) {
  43. this.coins = coins;
  44. }
  45. public Integer getGoods() {
  46. return goods;
  47. }
  48. public void setGoods(Integer goods) {
  49. this.goods = goods;
  50. }
  51. @Override
  52. public String toString() {
  53. return String.format(" %-8s => %10s\n %-8s => %10s\n %-8s => %10s\n",
  54. "id", this.id, "coins", this.coins, "goods", this.goods);
  55. }
  56. }
  57. /**
  58. * Main class for the basic Hibernate example.
  59. **/
  60. public class HibernateExample
  61. {
  62. public static class PlayerDAO {
  63. public static class NotEnoughException extends RuntimeException {
  64. public NotEnoughException(String message) {
  65. super(message);
  66. }
  67. }
  68. // Run SQL code in a way that automatically handles the
  69. // transaction retry logic so we don't have to duplicate it in
  70. // various places.
  71. public Object runTransaction(Session session, Function<Session, Object> fn) {
  72. Object resultObject = null;
  73. Transaction txn = session.beginTransaction();
  74. try {
  75. resultObject = fn.apply(session);
  76. txn.commit();
  77. System.out.println("APP: COMMIT;");
  78. } catch (JDBCException e) {
  79. System.out.println("APP: ROLLBACK BY JDBC ERROR;");
  80. txn.rollback();
  81. } catch (NotEnoughException e) {
  82. System.out.printf("APP: ROLLBACK BY LOGIC; %s", e.getMessage());
  83. txn.rollback();
  84. }
  85. return resultObject;
  86. }
  87. public Function<Session, Object> createPlayers(List<PlayerBean> players) throws JDBCException {
  88. return session -> {
  89. Integer addedPlayerAmount = 0;
  90. for (PlayerBean player: players) {
  91. session.persist(player);
  92. addedPlayerAmount ++;
  93. }
  94. System.out.printf("APP: createPlayers() --> %d\n", addedPlayerAmount);
  95. return addedPlayerAmount;
  96. };
  97. }
  98. public Function<Session, Object> buyGoods(String sellId, String buyId, Integer amount, Integer price) throws JDBCException {
  99. return session -> {
  100. PlayerBean sellPlayer = session.get(PlayerBean.class, sellId);
  101. PlayerBean buyPlayer = session.get(PlayerBean.class, buyId);
  102. if (buyPlayer == null || sellPlayer == null) {
  103. throw new NotEnoughException("sell or buy player not exist");
  104. }
  105. if (buyPlayer.getCoins() < price || sellPlayer.getGoods() < amount) {
  106. throw new NotEnoughException("coins or goods not enough, rollback");
  107. }
  108. buyPlayer.setGoods(buyPlayer.getGoods() + amount);
  109. buyPlayer.setCoins(buyPlayer.getCoins() - price);
  110. session.persist(buyPlayer);
  111. sellPlayer.setGoods(sellPlayer.getGoods() - amount);
  112. sellPlayer.setCoins(sellPlayer.getCoins() + price);
  113. session.persist(sellPlayer);
  114. System.out.printf("APP: buyGoods --> sell: %s, buy: %s, amount: %d, price: %d\n", sellId, buyId, amount, price);
  115. return 0;
  116. };
  117. }
  118. public Function<Session, Object> getPlayerByID(String id) throws JDBCException {
  119. return session -> session.get(PlayerBean.class, id);
  120. }
  121. public Function<Session, Object> printPlayers(Integer limit) throws JDBCException {
  122. return session -> {
  123. NativeQuery<PlayerBean> limitQuery = session.createNativeQuery("SELECT * FROM player_hibernate LIMIT :limit", PlayerBean.class);
  124. limitQuery.setParameter("limit", limit);
  125. List<PlayerBean> players = limitQuery.getResultList();
  126. for (PlayerBean player: players) {
  127. System.out.println("\n[printPlayers]:\n" + player);
  128. }
  129. return 0;
  130. };
  131. }
  132. public Function<Session, Object> countPlayers() throws JDBCException {
  133. return session -> {
  134. Query<Long> countQuery = session.createQuery("SELECT count(player_hibernate) FROM PlayerBean player_hibernate", Long.class);
  135. return countQuery.getSingleResult();
  136. };
  137. }
  138. }
  139. public static void main(String[] args) {
  140. // 1. Create a SessionFactory based on our hibernate.cfg.xml configuration
  141. // file, which defines how to connect to the database.
  142. SessionFactory sessionFactory
  143. = new Configuration()
  144. .configure("hibernate.cfg.xml")
  145. .addAnnotatedClass(PlayerBean.class)
  146. .buildSessionFactory();
  147. try (Session session = sessionFactory.openSession()) {
  148. // 2. And then, create DAO to manager your data.
  149. PlayerDAO playerDAO = new PlayerDAO();
  150. // 3. Run some simple examples.
  151. // Create a player who has 1 coin and 1 goods.
  152. playerDAO.runTransaction(session, playerDAO.createPlayers(Collections.singletonList(
  153. new PlayerBean("test", 1, 1))));
  154. // Get a player.
  155. PlayerBean testPlayer = (PlayerBean)playerDAO.runTransaction(session, playerDAO.getPlayerByID("test"));
  156. System.out.printf("PlayerDAO.getPlayer:\n => id: %s\n => coins: %s\n => goods: %s\n",
  157. testPlayer.getId(), testPlayer.getCoins(), testPlayer.getGoods());
  158. // Count players amount.
  159. Long count = (Long)playerDAO.runTransaction(session, playerDAO.countPlayers());
  160. System.out.printf("PlayerDAO.countPlayers:\n => %d total players\n", count);
  161. // Print 3 players.
  162. playerDAO.runTransaction(session, playerDAO.printPlayers(3));
  163. // 4. Explore more.
  164. // Player 1: id is "1", has only 100 coins.
  165. // Player 2: id is "2", has 114514 coins, and 20 goods.
  166. PlayerBean player1 = new PlayerBean("1", 100, 0);
  167. PlayerBean player2 = new PlayerBean("2", 114514, 20);
  168. // Create two players "by hand", using the INSERT statement on the backend.
  169. int addedCount = (Integer)playerDAO.runTransaction(session,
  170. playerDAO.createPlayers(Arrays.asList(player1, player2)));
  171. System.out.printf("PlayerDAO.createPlayers:\n => %d total inserted players\n", addedCount);
  172. // Player 1 wants to buy 10 goods from player 2.
  173. // It will cost 500 coins, but player 1 cannot afford it.
  174. System.out.println("\nPlayerDAO.buyGoods:\n => this trade will fail");
  175. Integer updatedCount = (Integer)playerDAO.runTransaction(session,
  176. playerDAO.buyGoods(player2.getId(), player1.getId(), 10, 500));
  177. System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount);
  178. // So player 1 has to reduce the incoming quantity to two.
  179. System.out.println("\nPlayerDAO.buyGoods:\n => this trade will success");
  180. updatedCount = (Integer)playerDAO.runTransaction(session,
  181. playerDAO.buyGoods(player2.getId(), player1.getId(), 2, 100));
  182. System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount);
  183. } finally {
  184. sessionFactory.close();
  185. }
  186. }
  187. }

进入目录 plain-java-jdbc

  1. cd plain-java-jdbc

目录结构如下所示:

  1. .
  2. ├── Makefile
  3. ├── plain-java-jdbc.iml
  4. ├── pom.xml
  5. └── src
  6. └── main
  7. ├── java
  8. └── com
  9. └── pingcap
  10. └── JDBCExample.java
  11. └── resources
  12. └── dbinit.sql

其中,dbinit.sql 为数据表初始化语句:

  1. USE test;
  2. DROP TABLE IF EXISTS player;
  3. CREATE TABLE player (
  4. `id` VARCHAR(36),
  5. `coins` INTEGER,
  6. `goods` INTEGER,
  7. PRIMARY KEY (`id`)
  8. );

JDBCExample.javaplain-java-jdbc 这个示例程序的主体。因为 TiDB 与 MySQL 协议兼容,因此,需要初始化一个 MySQL 协议的数据源 MysqlDataSource,以此连接到 TiDB。并在其后,初始化 PlayerDAO,用来管理数据对象,进行增删改查等操作。

PlayerDAO 是程序用来管理数据对象的类。其中 DAOData Access Object 的缩写。在其中定义了一系列数据的操作方法,用来对提供数据的写入能力。

PlayerBean 是数据实体类,为数据库表在程序内的映射。PlayerBean 的每个属性都对应着 player 表的一个字段。

  1. package com.pingcap;
  2. import com.mysql.cj.jdbc.MysqlDataSource;
  3. import java.sql.Connection;
  4. import java.sql.PreparedStatement;
  5. import java.sql.ResultSet;
  6. import java.sql.SQLException;
  7. import java.util.*;
  8. /**
  9. * Main class for the basic JDBC example.
  10. **/
  11. public class JDBCExample
  12. {
  13. public static class PlayerBean {
  14. private String id;
  15. private Integer coins;
  16. private Integer goods;
  17. public PlayerBean() {
  18. }
  19. public PlayerBean(String id, Integer coins, Integer goods) {
  20. this.id = id;
  21. this.coins = coins;
  22. this.goods = goods;
  23. }
  24. public String getId() {
  25. return id;
  26. }
  27. public void setId(String id) {
  28. this.id = id;
  29. }
  30. public Integer getCoins() {
  31. return coins;
  32. }
  33. public void setCoins(Integer coins) {
  34. this.coins = coins;
  35. }
  36. public Integer getGoods() {
  37. return goods;
  38. }
  39. public void setGoods(Integer goods) {
  40. this.goods = goods;
  41. }
  42. @Override
  43. public String toString() {
  44. return String.format(" %-8s => %10s\n %-8s => %10s\n %-8s => %10s\n",
  45. "id", this.id, "coins", this.coins, "goods", this.goods);
  46. }
  47. }
  48. /**
  49. * Data access object used by 'ExampleDataSource'.
  50. * Example for CURD and bulk insert.
  51. */
  52. public static class PlayerDAO {
  53. private final MysqlDataSource ds;
  54. private final Random rand = new Random();
  55. PlayerDAO(MysqlDataSource ds) {
  56. this.ds = ds;
  57. }
  58. /**
  59. * Create players by passing in a List of PlayerBean.
  60. *
  61. * @param players Will create players list
  62. * @return The number of create accounts
  63. */
  64. public int createPlayers(List<PlayerBean> players){
  65. int rows = 0;
  66. Connection connection = null;
  67. PreparedStatement preparedStatement = null;
  68. try {
  69. connection = ds.getConnection();
  70. preparedStatement = connection.prepareStatement("INSERT INTO player (id, coins, goods) VALUES (?, ?, ?)");
  71. } catch (SQLException e) {
  72. System.out.printf("[createPlayers] ERROR: { state => %s, cause => %s, message => %s }\n",
  73. e.getSQLState(), e.getCause(), e.getMessage());
  74. e.printStackTrace();
  75. return -1;
  76. }
  77. try {
  78. for (PlayerBean player : players) {
  79. preparedStatement.setString(1, player.getId());
  80. preparedStatement.setInt(2, player.getCoins());
  81. preparedStatement.setInt(3, player.getGoods());
  82. preparedStatement.execute();
  83. rows += preparedStatement.getUpdateCount();
  84. }
  85. } catch (SQLException e) {
  86. System.out.printf("[createPlayers] ERROR: { state => %s, cause => %s, message => %s }\n",
  87. e.getSQLState(), e.getCause(), e.getMessage());
  88. e.printStackTrace();
  89. } finally {
  90. try {
  91. connection.close();
  92. } catch (SQLException e) {
  93. e.printStackTrace();
  94. }
  95. }
  96. System.out.printf("\n[createPlayers]:\n '%s'\n", preparedStatement);
  97. return rows;
  98. }
  99. /**
  100. * Buy goods and transfer funds between one player and another in one transaction.
  101. * @param sellId Sell player id.
  102. * @param buyId Buy player id.
  103. * @param amount Goods amount, if sell player has not enough goods, the trade will break.
  104. * @param price Price should pay, if buy player has not enough coins, the trade will break.
  105. *
  106. * @return The number of effected players.
  107. */
  108. public int buyGoods(String sellId, String buyId, Integer amount, Integer price) {
  109. int effectPlayers = 0;
  110. Connection connection = null;
  111. try {
  112. connection = ds.getConnection();
  113. } catch (SQLException e) {
  114. System.out.printf("[buyGoods] ERROR: { state => %s, cause => %s, message => %s }\n",
  115. e.getSQLState(), e.getCause(), e.getMessage());
  116. e.printStackTrace();
  117. return effectPlayers;
  118. }
  119. try {
  120. connection.setAutoCommit(false);
  121. PreparedStatement playerQuery = connection.prepareStatement("SELECT * FROM player WHERE id=? OR id=? FOR UPDATE");
  122. playerQuery.setString(1, sellId);
  123. playerQuery.setString(2, buyId);
  124. playerQuery.execute();
  125. PlayerBean sellPlayer = null;
  126. PlayerBean buyPlayer = null;
  127. ResultSet playerQueryResultSet = playerQuery.getResultSet();
  128. while (playerQueryResultSet.next()) {
  129. PlayerBean player = new PlayerBean(
  130. playerQueryResultSet.getString("id"),
  131. playerQueryResultSet.getInt("coins"),
  132. playerQueryResultSet.getInt("goods")
  133. );
  134. System.out.println("\n[buyGoods]:\n 'check goods and coins enough'");
  135. System.out.println(player);
  136. if (sellId.equals(player.getId())) {
  137. sellPlayer = player;
  138. } else {
  139. buyPlayer = player;
  140. }
  141. }
  142. if (sellPlayer == null || buyPlayer == null) {
  143. throw new SQLException("player not exist.");
  144. }
  145. if (sellPlayer.getGoods().compareTo(amount) < 0) {
  146. throw new SQLException(String.format("sell player %s goods not enough.", sellId));
  147. }
  148. if (buyPlayer.getCoins().compareTo(price) < 0) {
  149. throw new SQLException(String.format("buy player %s coins not enough.", buyId));
  150. }
  151. PreparedStatement transfer = connection.prepareStatement("UPDATE player set goods = goods + ?, coins = coins + ? WHERE id=?");
  152. transfer.setInt(1, -amount);
  153. transfer.setInt(2, price);
  154. transfer.setString(3, sellId);
  155. transfer.execute();
  156. effectPlayers += transfer.getUpdateCount();
  157. transfer.setInt(1, amount);
  158. transfer.setInt(2, -price);
  159. transfer.setString(3, buyId);
  160. transfer.execute();
  161. effectPlayers += transfer.getUpdateCount();
  162. connection.commit();
  163. System.out.println("\n[buyGoods]:\n 'trade success'");
  164. } catch (SQLException e) {
  165. System.out.printf("[buyGoods] ERROR: { state => %s, cause => %s, message => %s }\n",
  166. e.getSQLState(), e.getCause(), e.getMessage());
  167. try {
  168. System.out.println("[buyGoods] Rollback");
  169. connection.rollback();
  170. } catch (SQLException ex) {
  171. // do nothing
  172. }
  173. } finally {
  174. try {
  175. connection.close();
  176. } catch (SQLException e) {
  177. // do nothing
  178. }
  179. }
  180. return effectPlayers;
  181. }
  182. /**
  183. * Get the player info by id.
  184. *
  185. * @param id Player id.
  186. * @return The player of this id.
  187. */
  188. public PlayerBean getPlayer(String id) {
  189. PlayerBean player = null;
  190. try (Connection connection = ds.getConnection()) {
  191. PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM player WHERE id = ?");
  192. preparedStatement.setString(1, id);
  193. preparedStatement.execute();
  194. ResultSet res = preparedStatement.executeQuery();
  195. if(!res.next()) {
  196. System.out.printf("No players in the table with id %s", id);
  197. } else {
  198. player = new PlayerBean(res.getString("id"), res.getInt("coins"), res.getInt("goods"));
  199. }
  200. } catch (SQLException e) {
  201. System.out.printf("PlayerDAO.getPlayer ERROR: { state => %s, cause => %s, message => %s }\n",
  202. e.getSQLState(), e.getCause(), e.getMessage());
  203. }
  204. return player;
  205. }
  206. /**
  207. * Insert randomized account data (id, coins, goods) using the JDBC fast path for
  208. * bulk inserts. The fastest way to get data into TiDB is using the
  209. * TiDB Lightning(https://docs.pingcap.com/tidb/stable/tidb-lightning-overview).
  210. * However, if you must bulk insert from the application using INSERT SQL, the best
  211. * option is the method shown here. It will require the following:
  212. *
  213. * Add `rewriteBatchedStatements=true` to your JDBC connection settings.
  214. * Setting rewriteBatchedStatements to true now causes CallableStatements
  215. * with batched arguments to be re-written in the form "CALL (...); CALL (...); ..."
  216. * to send the batch in as few client/server round trips as possible.
  217. * https://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-1-3.html
  218. *
  219. * You can see the `rewriteBatchedStatements` param effect logic at
  220. * implement function: `com.mysql.cj.jdbc.StatementImpl.executeBatchUsingMultiQueries`
  221. *
  222. * @param total Add players amount.
  223. * @param batchSize Bulk insert size for per batch.
  224. *
  225. * @return The number of new accounts inserted.
  226. */
  227. public int bulkInsertRandomPlayers(Integer total, Integer batchSize) {
  228. int totalNewPlayers = 0;
  229. try (Connection connection = ds.getConnection()) {
  230. // We're managing the commit lifecycle ourselves, so we can
  231. // control the size of our batch inserts.
  232. connection.setAutoCommit(false);
  233. // In this example we are adding 500 rows to the database,
  234. // but it could be any number. What's important is that
  235. // the batch size is 128.
  236. try (PreparedStatement pstmt = connection.prepareStatement("INSERT INTO player (id, coins, goods) VALUES (?, ?, ?)")) {
  237. for (int i=0; i<=(total/batchSize);i++) {
  238. for (int j=0; j<batchSize; j++) {
  239. String id = UUID.randomUUID().toString();
  240. pstmt.setString(1, id);
  241. pstmt.setInt(2, rand.nextInt(10000));
  242. pstmt.setInt(3, rand.nextInt(10000));
  243. pstmt.addBatch();
  244. }
  245. int[] count = pstmt.executeBatch();
  246. totalNewPlayers += count.length;
  247. System.out.printf("\nPlayerDAO.bulkInsertRandomPlayers:\n '%s'\n", pstmt);
  248. System.out.printf(" => %s row(s) updated in this batch\n", count.length);
  249. }
  250. connection.commit();
  251. } catch (SQLException e) {
  252. System.out.printf("PlayerDAO.bulkInsertRandomPlayers ERROR: { state => %s, cause => %s, message => %s }\n",
  253. e.getSQLState(), e.getCause(), e.getMessage());
  254. }
  255. } catch (SQLException e) {
  256. System.out.printf("PlayerDAO.bulkInsertRandomPlayers ERROR: { state => %s, cause => %s, message => %s }\n",
  257. e.getSQLState(), e.getCause(), e.getMessage());
  258. }
  259. return totalNewPlayers;
  260. }
  261. /**
  262. * Print a subset of players from the data store by limit.
  263. *
  264. * @param limit Print max size.
  265. */
  266. public void printPlayers(Integer limit) {
  267. try (Connection connection = ds.getConnection()) {
  268. PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM player LIMIT ?");
  269. preparedStatement.setInt(1, limit);
  270. preparedStatement.execute();
  271. ResultSet res = preparedStatement.executeQuery();
  272. while (!res.next()) {
  273. PlayerBean player = new PlayerBean(res.getString("id"),
  274. res.getInt("coins"), res.getInt("goods"));
  275. System.out.println("\n[printPlayers]:\n" + player);
  276. }
  277. } catch (SQLException e) {
  278. System.out.printf("PlayerDAO.printPlayers ERROR: { state => %s, cause => %s, message => %s }\n",
  279. e.getSQLState(), e.getCause(), e.getMessage());
  280. }
  281. }
  282. /**
  283. * Count players from the data store.
  284. *
  285. * @return All players count
  286. */
  287. public int countPlayers() {
  288. int count = 0;
  289. try (Connection connection = ds.getConnection()) {
  290. PreparedStatement preparedStatement = connection.prepareStatement("SELECT count(*) FROM player");
  291. preparedStatement.execute();
  292. ResultSet res = preparedStatement.executeQuery();
  293. if(res.next()) {
  294. count = res.getInt(1);
  295. }
  296. } catch (SQLException e) {
  297. System.out.printf("PlayerDAO.countPlayers ERROR: { state => %s, cause => %s, message => %s }\n",
  298. e.getSQLState(), e.getCause(), e.getMessage());
  299. }
  300. return count;
  301. }
  302. }
  303. public static void main(String[] args) {
  304. // 1. Configure the example database connection.
  305. // 1.1 Create a mysql data source instance.
  306. MysqlDataSource mysqlDataSource = new MysqlDataSource();
  307. // 1.2 Set server name, port, database name, username and password.
  308. mysqlDataSource.setServerName("localhost");
  309. mysqlDataSource.setPortNumber(4000);
  310. mysqlDataSource.setDatabaseName("test");
  311. mysqlDataSource.setUser("root");
  312. mysqlDataSource.setPassword("");
  313. // Or you can use jdbc string instead.
  314. // mysqlDataSource.setURL("jdbc:mysql://{host}:{port}/test?user={user}&password={password}");
  315. // 2. And then, create DAO to manager your data.
  316. PlayerDAO dao = new PlayerDAO(mysqlDataSource);
  317. // 3. Run some simple examples.
  318. // Create a player, who has a coin and a goods..
  319. dao.createPlayers(Collections.singletonList(new PlayerBean("test", 1, 1)));
  320. // Get a player.
  321. PlayerBean testPlayer = dao.getPlayer("test");
  322. System.out.printf("PlayerDAO.getPlayer:\n => id: %s\n => coins: %s\n => goods: %s\n",
  323. testPlayer.getId(), testPlayer.getCoins(), testPlayer.getGoods());
  324. // Create players with bulk inserts. Insert 1919 players totally, with 114 players per batch.
  325. int addedCount = dao.bulkInsertRandomPlayers(1919, 114);
  326. System.out.printf("PlayerDAO.bulkInsertRandomPlayers:\n => %d total inserted players\n", addedCount);
  327. // Count players amount.
  328. int count = dao.countPlayers();
  329. System.out.printf("PlayerDAO.countPlayers:\n => %d total players\n", count);
  330. // Print 3 players.
  331. dao.printPlayers(3);
  332. // 4. Explore more.
  333. // Player 1: id is "1", has only 100 coins.
  334. // Player 2: id is "2", has 114514 coins, and 20 goods.
  335. PlayerBean player1 = new PlayerBean("1", 100, 0);
  336. PlayerBean player2 = new PlayerBean("2", 114514, 20);
  337. // Create two players "by hand", using the INSERT statement on the backend.
  338. addedCount = dao.createPlayers(Arrays.asList(player1, player2));
  339. System.out.printf("PlayerDAO.createPlayers:\n => %d total inserted players\n", addedCount);
  340. // Player 1 wants to buy 10 goods from player 2.
  341. // It will cost 500 coins, but player 1 cannot afford it.
  342. System.out.println("\nPlayerDAO.buyGoods:\n => this trade will fail");
  343. int updatedCount = dao.buyGoods(player2.getId(), player1.getId(), 10, 500);
  344. System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount);
  345. // So player 1 has to reduce the incoming quantity to two.
  346. System.out.println("\nPlayerDAO.buyGoods:\n => this trade will success");
  347. updatedCount = dao.buyGoods(player2.getId(), player1.getId(), 2, 100);
  348. System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount);
  349. }
  350. }

第 3 步:运行代码

本节将逐步介绍代码的运行方法。

第 3 步第 1 部分:JDBC 表初始化

  • 使用 Mybatis(推荐)
  • 使用 Hibernate(推荐)
  • 使用 JDBC

Java - 图4

小贴士

在 Gitpod Playground 中尝试 Mybatis:现在就试试

使用 Mybatis 时,需手动初始化数据库表。若你本地已经安装了 mysql-client,且使用本地集群,可直接在 plain-java-mybatis 目录下通过 make prepare 运行:

  1. make prepare

或直接执行:

  1. mysql --host 127.0.0.1 --port 4000 -u root < src/main/resources/dbinit.sql

若你不使用本地集群,或未安装 mysql-client,请直接登录你的集群,并运行 src/main/resources/dbinit.sql 文件内的 SQL 语句。

Java - 图5

小贴士

在 Gitpod Playground 中尝试 Hibernate:现在就试试

无需手动初始化表。

Java - 图6

小贴士

在 Gitpod Playground 中尝试 JDBC:现在就试试

使用 JDBC 时,需手动初始化数据库表,若你本地已经安装了 mysql-client,且使用本地集群,可直接在 plain-java-jdbc 目录下运行:

  1. make mysql

或直接执行:

  1. mysql --host 127.0.0.1 --port 4000 -u root<src/main/resources/dbinit.sql

若你不使用本地集群,或未安装 mysql-client,请直接登录你的集群,并运行 src/main/resources/dbinit.sql 文件内的 SQL 语句。

第 3 步第 2 部分:TiDB Cloud 更改参数

  • 使用 Mybatis(推荐)
  • 使用 Hibernate(推荐)
  • 使用 JDBC

若你使用 TiDB Cloud Serverless Tier 集群,更改 mybatis-config.xml 内关于 dataSource.urldataSource.usernamedataSource.password 的参数:

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE configuration
  3. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  4. "http://mybatis.org/dtd/mybatis-3-config.dtd">
  5. <configuration>
  6. <settings>
  7. <setting name="cacheEnabled" value="true"/>
  8. <setting name="lazyLoadingEnabled" value="false"/>
  9. <setting name="aggressiveLazyLoading" value="true"/>
  10. <setting name="logImpl" value="LOG4J"/>
  11. </settings>
  12. <typeAliases>
  13. <package name="com.pingcap.dao"/>
  14. </typeAliases>
  15. <environments default="development">
  16. <environment id="development">
  17. <!-- JDBC transaction manager -->
  18. <transactionManager type="JDBC"/>
  19. <!-- Database pool -->
  20. <dataSource type="POOLED">
  21. <property name="driver" value="com.mysql.jdbc.Driver"/>
  22. <property name="url" value="jdbc:mysql://127.0.0.1:4000/test"/>
  23. <property name="username" value="root"/>
  24. <property name="password" value=""/>
  25. </dataSource>
  26. </environment>
  27. </environments>
  28. <mappers>
  29. <mapper resource="mapper/PlayerMapper.xml"/>
  30. <mapper resource="mapper/PlayerMapperEx.xml"/>
  31. </mappers>
  32. </configuration>

若你设定的密码为 123456,而且从 TiDB Cloud Serverless Tier 集群面板中得到的连接信息为:

  • Endpoint: xxx.tidbcloud.com
  • Port: 4000
  • User: 2aEp24QWEDLqRFs.root

那么此处应将配置文件中 dataSource 节点内更改为:

  1. <?xml version="1.0" encoding="UTF-8" ?>
  2. <!DOCTYPE configuration
  3. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  4. "http://mybatis.org/dtd/mybatis-3-config.dtd">
  5. ...
  6. <!-- Database pool -->
  7. <dataSource type="POOLED">
  8. <property name="driver" value="com.mysql.jdbc.Driver"/>
  9. <property name="url" value="jdbc:mysql://xxx.tidbcloud.com:4000/test?sslMode=VERIFY_IDENTITY&amp;enabledTLSProtocols=TLSv1.2,TLSv1.3"/>
  10. <property name="username" value="2aEp24QWEDLqRFs.root"/>
  11. <property name="password" value="123456"/>
  12. </dataSource>
  13. ...
  14. </configuration>

若你使用 TiDB Cloud Serverless Tier 集群,更改 hibernate.cfg.xml 内关于 hibernate.connection.urlhibernate.connection.usernamehibernate.connection.password 的参数:

  1. <?xml version='1.0' encoding='utf-8'?>
  2. <!DOCTYPE hibernate-configuration PUBLIC
  3. "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  4. "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
  5. <hibernate-configuration>
  6. <session-factory>
  7. <!-- Database connection settings -->
  8. <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
  9. <property name="hibernate.dialect">org.hibernate.dialect.TiDBDialect</property>
  10. <property name="hibernate.connection.url">jdbc:mysql://localhost:4000/test</property>
  11. <property name="hibernate.connection.username">root</property>
  12. <property name="hibernate.connection.password"></property>
  13. <property name="hibernate.connection.autocommit">false</property>
  14. <!-- Required so a table can be created from the 'PlayerDAO' class -->
  15. <property name="hibernate.hbm2ddl.auto">create-drop</property>
  16. <!-- Optional: Show SQL output for debugging -->
  17. <property name="hibernate.show_sql">true</property>
  18. <property name="hibernate.format_sql">true</property>
  19. </session-factory>
  20. </hibernate-configuration>

若你设定的密码为 123456,而且从 TiDB Cloud Serverless Tier 集群面板中得到的连接信息为:

  • Endpoint: xxx.tidbcloud.com
  • Port: 4000
  • User: 2aEp24QWEDLqRFs.root

那么此处应将配置文件更改为:

  1. <?xml version='1.0' encoding='utf-8'?>
  2. <!DOCTYPE hibernate-configuration PUBLIC
  3. "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
  4. "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">
  5. <hibernate-configuration>
  6. <session-factory>
  7. <!-- Database connection settings -->
  8. <property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
  9. <property name="hibernate.dialect">org.hibernate.dialect.TiDBDialect</property>
  10. <property name="hibernate.connection.url">jdbc:mysql://xxx.tidbcloud.com:4000/test?sslMode=VERIFY_IDENTITY&amp;enabledTLSProtocols=TLSv1.2,TLSv1.3</property>
  11. <property name="hibernate.connection.username">2aEp24QWEDLqRFs.root</property>
  12. <property name="hibernate.connection.password">123456</property>
  13. <property name="hibernate.connection.autocommit">false</property>
  14. <!-- Required so a table can be created from the 'PlayerDAO' class -->
  15. <property name="hibernate.hbm2ddl.auto">create-drop</property>
  16. <!-- Optional: Show SQL output for debugging -->
  17. <property name="hibernate.show_sql">true</property>
  18. <property name="hibernate.format_sql">true</property>
  19. </session-factory>
  20. </hibernate-configuration>

若你使用 TiDB Cloud Serverless Tier 集群,更改 JDBCExample.java 内关于 Host、Port、User、Password 的参数:

  1. mysqlDataSource.setServerName("localhost");
  2. mysqlDataSource.setPortNumber(4000);
  3. mysqlDataSource.setDatabaseName("test");
  4. mysqlDataSource.setUser("root");
  5. mysqlDataSource.setPassword("");

若你设定的密码为 123456,而且从 TiDB Cloud Serverless Tier 集群面板中得到的连接信息为:

  • Endpoint: xxx.tidbcloud.com
  • Port: 4000
  • User: 2aEp24QWEDLqRFs.root

那么此处应将参数更改为:

  1. mysqlDataSource.setServerName("xxx.tidbcloud.com");
  2. mysqlDataSource.setPortNumber(4000);
  3. mysqlDataSource.setDatabaseName("test");
  4. mysqlDataSource.setUser("2aEp24QWEDLqRFs.root");
  5. mysqlDataSource.setPassword("123456");
  6. mysqlDataSource.setSslMode(PropertyDefinitions.SslMode.VERIFY_IDENTITY.name());
  7. mysqlDataSource.setEnabledTLSProtocols("TLSv1.2,TLSv1.3");

第 3 步第 3 部分:运行

  • 使用 Mybatis(推荐)
  • 使用 Hibernate(推荐)
  • 使用 JDBC

运行 make,这是以下四个操作的组合:

  • 创建表 (make prepare):

    1. mysql --host 127.0.0.1 --port 4000 -u root < src/main/resources/dbinit.sql
    2. mysql --host 127.0.0.1 --port 4000 -u root -e "TRUNCATE test.player"
  • 清理并构建 (make gen):

    1. rm -f src/main/java/com/pingcap/model/Player.java
    2. rm -f src/main/java/com/pingcap/model/PlayerMapper.java
    3. rm -f src/main/resources/mapper/PlayerMapper.xml
    4. mvn mybatis-generator:generate
  • 清理并构建 (make build):mvn clean package

  • 运行 (make run):java -jar target/plain-java-mybatis-0.0.1-jar-with-dependencies.jar

你也可以单独运行这四个 make 命令或原生命令。

运行 make,这是以下两个操作的组合:

  • 清理并构建 (make build):mvn clean package
  • 运行 (make run):java -jar target/plain-java-hibernate-0.0.1-jar-with-dependencies.jar

你也可以单独运行这两个 make 命令或原生命令。

运行 make,这是以下两个操作的组合:

  • 清理并构建 (make build): mvn clean package
  • 运行 (make run): java -jar target/plain-java-jdbc-0.0.1-jar-with-dependencies.jar

你也可以单独运行这两个 make 命令或原生命令。

第 4 步:预期输出

  • 使用 Mybatis(推荐)
  • 使用 Hibernate(推荐)
  • 使用 JDBC

Mybatis 预期输出

Hibernate 预期输出

JDBC 预期输出