diff --git a/develop/dev-guide-playground-gitpod.md b/develop/dev-guide-playground-gitpod.md index 531143c24d4a..283bc1bc931f 100644 --- a/develop/dev-guide-playground-gitpod.md +++ b/develop/dev-guide-playground-gitpod.md @@ -40,8 +40,6 @@ Gitpod 是一个开源 Kubernetes 应用程序(GitHub 仓库地址 - +# 使用 Hibernate 连接到 TiDB -# TiDB 和 Hibernate 的简单 CRUD 应用程序 +TiDB 是一个兼容 MySQL 的数据库。[Hibernate](https://hibernate.org/orm/) 是当前比较流行的开源 Java 应用持久层框架,且 Hibernate 在版本 `6.0.0.Beta2` 及以上支持了 TiDB 方言,完美适配了 TiDB 的特性。 -[Hibernate](https://hibernate.org/) 是当前比较流行的开源 Java 应用持久层框架,且 Hibernate 在版本 `6.0.0.Beta2` 及以后支持了 TiDB 方言,完美适配了 TiDB 的特性。 +本文档将展示如何使用 TiDB 和 Hibernate 来完成以下任务: -本文档将展示如何使用 TiDB 和 Java 来构造一个简单的 CRUD 应用程序。 +- 配置你的环境。 +- 使用 Hibernate 连接到 TiDB 集群。 +- 构建并运行你的应用程序。你也可以参考[示例代码片段](#示例代码片段),完成基本的 CRUD 操作。 -> **注意:** +> **注意** > -> 推荐使用 Java 8 及以上版本进行 TiDB 的应用程序的编写。 +> 本文档适用于 TiDB Serverless、TiDB Dedicated 和本地部署的 TiDB。 -## 拓展学习视频 +## 前置需求 -- [使用 Connector/J - TiDB v6](https://learn.pingcap.com/learner/course/840002/?utm_source=docs-cn-dev-guide) -- [在 TiDB 上开发应用的最佳实践 - TiDB v6](https://learn.pingcap.com/learner/course/780002/?utm_source=docs-cn-dev-guide) +- 推荐 **Java Development Kit** (JDK) **17** 及以上版本。你可以根据公司及个人需求,自行选择 [OpenJDK](https://openjdk.org/) 或 [Oracle JDK](https://www.oracle.com/hk/java/technologies/downloads/)。 +- [Maven](https://maven.apache.org/install.html) **3.8** 及以上版本。 +- [Git](https://git-scm.com/downloads)。 +- TiDB 集群。如果你还没有 TiDB 集群,可以按照以下方式创建: + - (推荐方式)参考[创建 TiDB Serverless 集群](/develop/dev-guide-build-cluster-in-cloud.md#第-1-步创建-tidb-serverless-集群),创建你自己的 TiDB Cloud 集群。 + - 参考[部署本地测试 TiDB 集群](/quick-start-with-tidb.md#部署本地测试集群)或[部署正式 TiDB 集群](/production-deployment-using-tiup.md),创建本地集群。 -## 第 1 步:启动你的 TiDB 集群 +## 运行代码并连接到 TiDB -本节将介绍 TiDB 集群的启动方法。 +本小节演示如何运行示例应用程序的代码,并连接到 TiDB。 -**使用 TiDB Serverless 集群** +### 第 1 步:克隆示例代码仓库到本地 -详细步骤,请参考:[创建 TiDB Serverless 集群](/develop/dev-guide-build-cluster-in-cloud.md#第-1-步创建-tidb-serverless-集群)。 +运行以下命令,将示例代码仓库克隆到本地: -**使用本地集群** +```bash +git clone https://github.com/tidb-samples/tidb-java-hibernate-quickstart.git +cd tidb-java-hibernate-quickstart +``` -详细步骤,请参考:[部署本地测试 TiDB 集群](/quick-start-with-tidb.md#部署本地测试集群)或[部署正式 TiDB 集群](/production-deployment-using-tiup.md)。 +### 第 2 步:配置连接信息 -## 第 2 步:获取代码 +根据不同的 TiDB 部署方式,使用不同的方法连接到 TiDB 集群。 -```shell -git clone https://github.com/pingcap-inc/tidb-example-java.git -``` + -与 [Hibernate](https://hibernate.org/orm/) 对比,JDBC 的实现方式并非最优体验。你需要自行编写错误处理逻辑,并且代码无法简单复用。这会使你的代码有些冗余。 +
-此处将以 `6.0.0.Beta2` 版本进行说明。 +1. 在 TiDB Cloud 的 [**Clusters**](https://tidbcloud.com/console/clusters) 页面中,选择你的 TiDB Serverless 集群,进入集群的 **Overview** 页面。 -进入目录 `plain-java-hibernate`: +2. 点击右上角的 **Connect** 按钮,将会弹出连接对话框。 -```shell -cd plain-java-hibernate -``` +3. 确认对话框中的配置和你的运行环境一致。 -目录结构如下所示: + - **Endpoint Type** 为 `Public`。 + - **Connect With** 选择 `General`。 + - **Operating System** 为你的运行环境。 -``` -. -├── Makefile -├── plain-java-hibernate.iml -├── pom.xml -└── src - └── main - ├── java - │ └── com - │ └── pingcap - │ └── HibernateExample.java - └── resources - └── hibernate.cfg.xml -``` + > **Tip:** + > + > 如果你在 Windows Subsystem for Linux (WSL) 中运行,请切换为对应的 Linux 发行版。 -其中,`hibernate.cfg.xml` 为 Hibernate 配置文件,定义了: +4. 如果你还没有设置密码,点击 **Create password** 生成一个随机密码。 -```xml - - - - + > **Tip:** + > + > 如果你之前已经生成过密码,可以直接使用原密码,或点击 **Reset password** 重新生成密码。 - - com.mysql.cj.jdbc.Driver - org.hibernate.dialect.TiDBDialect - jdbc:mysql://localhost:4000/test - root - - false +5. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - - create-drop + ```bash + cp env.sh.example env.sh + ``` - - true - true - - -``` +6. 复制并粘贴对应连接字符串至 `env.sh` 中。需更改部分示例结果如下: -`HibernateExample.java` 是 `plain-java-hibernate` 这个示例程序的主体。使用 Hibernate 时,相较于 JDBC,这里仅需写入配置文件地址,Hibernate 屏蔽了创建数据库连接时,不同数据库差异的细节。 + ```shell + export TIDB_HOST='{host}' # e.g. gateway01.ap-northeast-1.prod.aws.tidbcloud.com + export TIDB_PORT='4000' + export TIDB_USER='{user}' # e.g. xxxxxx.root + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='true' + ``` -`PlayerDAO` 是程序用来管理数据对象的类。其中 `DAO` 是 [Data Access Object](https://en.wikipedia.org/wiki/Data_access_object) 的缩写。其中定义了一系列数据的操作方法,用来提供数据的写入能力。相较于 JDBC,Hibernate 封装了大量的操作,如对象映射、基本对象的 CRUD 等,极大地简化了代码量。 + 注意替换 `{}` 中的占位符为连接对话框中获得的值。 -`PlayerBean` 是数据实体类,为数据库表在程序内的映射。`PlayerBean` 的每个属性都对应着 `player` 表的一个字段。相较于 JDBC,Hibernate 的 `PlayerBean` 实体类为了给 Hibernate 提供更多的信息,加入了注解,用来指示映射关系。 + TiDB Serverless 要求使用 TLS (SSL) connection,因此 `USE_SSL` 的值应为 `true`。 -```java -package com.pingcap; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import jakarta.persistence.Table; -import org.hibernate.JDBCException; -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.Transaction; -import org.hibernate.cfg.Configuration; -import org.hibernate.query.NativeQuery; -import org.hibernate.query.Query; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.function.Function; - -@Entity -@Table(name = "player_hibernate") -class PlayerBean { - @Id - private String id; - @Column(name = "coins") - private Integer coins; - @Column(name = "goods") - private Integer goods; - - public PlayerBean() { - } - - public PlayerBean(String id, Integer coins, Integer goods) { - this.id = id; - this.coins = coins; - this.goods = goods; - } - - public String getId() { - return id; - } - - public void setId(String 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; - } - - @Override - public String toString() { - return String.format(" %-8s => %10s\n %-8s => %10s\n %-8s => %10s\n", - "id", this.id, "coins", this.coins, "goods", this.goods); - } -} +7. 保存 `env.sh` 文件。 -/** - * Main class for the basic Hibernate example. - **/ -public class HibernateExample -{ - public static class PlayerDAO { - public static class NotEnoughException extends RuntimeException { - public NotEnoughException(String message) { - super(message); - } - } - - // Run SQL code in a way that automatically handles the - // transaction retry logic so we don't have to duplicate it in - // various places. - public Object runTransaction(Session session, Function fn) { - Object resultObject = null; - - Transaction txn = session.beginTransaction(); - try { - resultObject = fn.apply(session); - txn.commit(); - System.out.println("APP: COMMIT;"); - } catch (JDBCException e) { - System.out.println("APP: ROLLBACK BY JDBC ERROR;"); - txn.rollback(); - } catch (NotEnoughException e) { - System.out.printf("APP: ROLLBACK BY LOGIC; %s", e.getMessage()); - txn.rollback(); - } - return resultObject; - } - - public Function createPlayers(List players) throws JDBCException { - return session -> { - Integer addedPlayerAmount = 0; - for (PlayerBean player: players) { - session.persist(player); - addedPlayerAmount ++; - } - System.out.printf("APP: createPlayers() --> %d\n", addedPlayerAmount); - return addedPlayerAmount; - }; - } - - public Function buyGoods(String sellId, String buyId, Integer amount, Integer price) throws JDBCException { - return session -> { - PlayerBean sellPlayer = session.get(PlayerBean.class, sellId); - PlayerBean buyPlayer = session.get(PlayerBean.class, buyId); - - if (buyPlayer == null || sellPlayer == null) { - throw new NotEnoughException("sell or buy player not exist"); - } - - if (buyPlayer.getCoins() < price || sellPlayer.getGoods() < amount) { - throw new NotEnoughException("coins or goods not enough, rollback"); - } - - buyPlayer.setGoods(buyPlayer.getGoods() + amount); - buyPlayer.setCoins(buyPlayer.getCoins() - price); - session.persist(buyPlayer); - - sellPlayer.setGoods(sellPlayer.getGoods() - amount); - sellPlayer.setCoins(sellPlayer.getCoins() + price); - session.persist(sellPlayer); - - System.out.printf("APP: buyGoods --> sell: %s, buy: %s, amount: %d, price: %d\n", sellId, buyId, amount, price); - return 0; - }; - } - - public Function getPlayerByID(String id) throws JDBCException { - return session -> session.get(PlayerBean.class, id); - } - - public Function printPlayers(Integer limit) throws JDBCException { - return session -> { - NativeQuery limitQuery = session.createNativeQuery("SELECT * FROM player_hibernate LIMIT :limit", PlayerBean.class); - limitQuery.setParameter("limit", limit); - List players = limitQuery.getResultList(); - - for (PlayerBean player: players) { - System.out.println("\n[printPlayers]:\n" + player); - } - return 0; - }; - } - - public Function countPlayers() throws JDBCException { - return session -> { - Query countQuery = session.createQuery("SELECT count(player_hibernate) FROM PlayerBean player_hibernate", Long.class); - return countQuery.getSingleResult(); - }; - } - } - - public static void main(String[] args) { - // 1. Create a SessionFactory based on our hibernate.cfg.xml configuration - // file, which defines how to connect to the database. - SessionFactory sessionFactory - = new Configuration() - .configure("hibernate.cfg.xml") - .addAnnotatedClass(PlayerBean.class) - .buildSessionFactory(); - - try (Session session = sessionFactory.openSession()) { - // 2. And then, create DAO to manager your data. - PlayerDAO playerDAO = new PlayerDAO(); - - // 3. Run some simple examples. - - // Create a player who has 1 coin and 1 goods. - playerDAO.runTransaction(session, playerDAO.createPlayers(Collections.singletonList( - new PlayerBean("test", 1, 1)))); - - // Get a player. - PlayerBean testPlayer = (PlayerBean)playerDAO.runTransaction(session, playerDAO.getPlayerByID("test")); - System.out.printf("PlayerDAO.getPlayer:\n => id: %s\n => coins: %s\n => goods: %s\n", - testPlayer.getId(), testPlayer.getCoins(), testPlayer.getGoods()); - - // Count players amount. - Long count = (Long)playerDAO.runTransaction(session, playerDAO.countPlayers()); - System.out.printf("PlayerDAO.countPlayers:\n => %d total players\n", count); - - // Print 3 players. - playerDAO.runTransaction(session, playerDAO.printPlayers(3)); - - // 4. Explore more. - - // Player 1: id is "1", has only 100 coins. - // Player 2: id is "2", has 114514 coins, and 20 goods. - PlayerBean player1 = new PlayerBean("1", 100, 0); - PlayerBean player2 = new PlayerBean("2", 114514, 20); - - // Create two players "by hand", using the INSERT statement on the backend. - int addedCount = (Integer)playerDAO.runTransaction(session, - playerDAO.createPlayers(Arrays.asList(player1, player2))); - System.out.printf("PlayerDAO.createPlayers:\n => %d total inserted players\n", addedCount); - - // Player 1 wants to buy 10 goods from player 2. - // It will cost 500 coins, but player 1 cannot afford it. - System.out.println("\nPlayerDAO.buyGoods:\n => this trade will fail"); - Integer updatedCount = (Integer)playerDAO.runTransaction(session, - playerDAO.buyGoods(player2.getId(), player1.getId(), 10, 500)); - System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount); - - // So player 1 has to reduce the incoming quantity to two. - System.out.println("\nPlayerDAO.buyGoods:\n => this trade will success"); - updatedCount = (Integer)playerDAO.runTransaction(session, - playerDAO.buyGoods(player2.getId(), player1.getId(), 2, 100)); - System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount); - } finally { - sessionFactory.close(); - } - } -} -``` +
-## 第 3 步:运行代码 +
-本节将逐步介绍代码的运行方法。 +1. 在 TiDB Cloud 的 [**Clusters**](https://tidbcloud.com/console/clusters) 页面中,选择你的 TiDB Dedicated 集群,进入集群的 **Overview** 页面。 -### 第 3 步第 1 部分:TiDB Cloud 更改参数 +2. 点击右上角的 **Connect** 按钮,将会出现连接对话框。 -若你使用 TiDB Serverless 集群,更改 `hibernate.cfg.xml` 内关于 `hibernate.connection.url`、`hibernate.connection.username`、`hibernate.connection.password` 的参数: +3. 在对话框中点击 **Allow Access from Anywhere**。 -```xml - - - - + 更多配置细节,可参考 [TiDB Dedicated 标准连接教程(英文)](https://docs.pingcap.com/tidbcloud/connect-via-standard-connection)。 - - com.mysql.cj.jdbc.Driver - org.hibernate.dialect.TiDBDialect - jdbc:mysql://localhost:4000/test - root - - false +4. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - - create-drop + ```bash + cp env.sh.example env.sh + ``` - - true - true - - -``` +5. 复制并粘贴对应的连接字符串至 `env.sh` 中。需更改部分示例结果如下: + + ```shell + export TIDB_HOST='{host}' # e.g. tidb.xxxx.clusters.tidb-cloud.com + export TIDB_PORT='4000' + export TIDB_USER='{user}' # e.g. root + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='false' + ``` + + 注意替换 `{}` 中的占位符为连接对话框中获得的值。 + +6. 保存 `env.sh` 文件。 + +
+ +
+ +1. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: + + ```bash + cp env.sh.example env.sh + ``` + +2. 复制并粘贴对应 TiDB 的连接字符串至 `env.sh` 中。需更改部分示例结果如下: + + ```shell + export TIDB_HOST='{host}' + export TIDB_PORT='4000' + export TIDB_USER='root' + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='false' + ``` + + 注意替换 `{}` 中的占位符为你的 TiDB 对应的值,并设置 `USE_SSL` 为 `false`。如果你在本机运行 TiDB,默认 Host 地址为 `127.0.0.1`,密码为空。 + +3. 保存 `env.sh` 文件。 + +
+ +
+ +### 第 3 步:运行代码并查看结果 + +1. 运行下述命令,执行示例代码: + + ```shell + make + ``` -若你设定的密码为 `123456`,而且从 TiDB Serverless 集群面板中得到的连接信息为: +2. 查看 [`Expected-Output.txt`](https://github.com/tidb-samples/tidb-java-hibernate-quickstart/blob/main/Expected-Output.txt),并与你的程序输出进行比较。结果近似即为连接成功。 -- Endpoint: `xxx.tidbcloud.com` -- Port: `4000` -- User: `2aEp24QWEDLqRFs.root` +## 示例代码片段 -那么此处应将配置文件更改为: +你可参考以下关键代码片段,完成自己的应用开发。 + +完整代码及其运行方式,见代码仓库 [tidb-java-hibernate-quickstart](https://github.com/tidb-samples/tidb-java-hibernate-quickstart/blob/main/README-zh.md)。 + +### 连接到 TiDB + +编写配置文件 `hibernate.cfg.xml`: ```xml @@ -381,9 +183,9 @@ public class HibernateExample com.mysql.cj.jdbc.Driver org.hibernate.dialect.TiDBDialect - jdbc:mysql://xxx.tidbcloud.com:4000/test?sslMode=VERIFY_IDENTITY&enabledTLSProtocols=TLSv1.2,TLSv1.3 - 2aEp24QWEDLqRFs.root - 123456 + ${tidb_jdbc_url} + ${tidb_user} + ${tidb_password} false @@ -396,24 +198,57 @@ public class HibernateExample ``` -### 第 3 步第 2 部分:运行 +请将 `${tidb_jdbc_url}`、`${tidb_user}`、`${tidb_password}` 等替换为你的 TiDB 集群的实际值。随后编写以下函数: -你可以分别运行 `make build` 和 `make run` 以运行此代码: +```java +public SessionFactory getSessionFactory() { + return new Configuration() + .configure("hibernate.cfg.xml") + .addAnnotatedClass(${your_entity_class}) + .buildSessionFactory(); +} +``` -```shell -make build # this command executes `mvn clean package` -make run # this command executes `java -jar target/plain-java-hibernate-0.0.1-jar-with-dependencies.jar` +在使用该函数时,你需要替换 `${your_entity_class}` 为自己的数据实体类。如果你有多个实体类,需要添加多个 `.addAnnotatedClass(${your_entity_class})` 语句。此外,这仅是 Hibernate 的其中一种配置方式。在配置中遇到任何问题,或想了解更多关于 Hibernate 的信息,你可参考 [Hibernate 官方文档](https://hibernate.org/orm/documentation)。 + +### 插入或更新数据 + +```java +try (Session session = sessionFactory.openSession()) { + session.persist(new PlayerBean("id", 1, 1)); +} ``` -或者你也可以直接使用原生的命令: +更多信息参考[插入数据](/develop/dev-guide-insert-data.md)和[更新数据](/develop/dev-guide-update-data.md)。 + +### 查询数据 -```shell -mvn clean package -java -jar target/plain-java-hibernate-0.0.1-jar-with-dependencies.jar +```java +try (Session session = sessionFactory.openSession()) { + PlayerBean player = session.get(PlayerBean.class, "id"); + System.out.println(player); +} +``` + +更多信息参考[查询数据](/develop/dev-guide-get-data-from-single-table.md)。 + +### 删除数据 + +```java +try (Session session = sessionFactory.openSession()) { + session.remove(new PlayerBean("id", 1, 1)); +} ``` -再或者直接运行 `make` 命令,这是 `make build` 和 `make run` 的组合。 +更多信息参考[删除数据](/develop/dev-guide-delete-data.md)。 + +## 下一步 + +- 关于 Hibernate 的更多使用方法,可以参考 [Hibernate 官方文档](https://hibernate.org/orm/documentation)。 +- 你可以继续阅读开发者文档,以获取更多关于 TiDB 应用开发的最佳实践。例如:[插入数据](/develop/dev-guide-insert-data.md)、[更新数据](/develop/dev-guide-update-data.md)、[删除数据](/develop/dev-guide-delete-data.md)、[单表读取](/develop/dev-guide-get-data-from-single-table.md)、[事务](/develop/dev-guide-transaction-overview.md)、[SQL 性能优化](/develop/dev-guide-optimize-sql-overview.md)等。 +- 如果你更倾向于参与课程进行学习,我们也提供专业的 [TiDB 开发者课程](https://cn.pingcap.com/courses-catalog/back-end-developer/?utm_source=docs-cn-dev-guide)支持,并在考试后提供相应的[资格认证](https://learn.pingcap.com/learner/certification-center)。 +- 我们还额外提供针对 Java 开发者的课程:[使用 Connector/J - TiDB v6](https://learn.pingcap.com/learner/course/840002/?utm_source=docs-cn-dev-guide) 及[在 TiDB 上开发应用的最佳实践 - TiDB v6](https://learn.pingcap.com/learner/course/780002/?utm_source=docs-cn-dev-guide)。 -## 第 4 步:预期输出 +## 需要帮助? -[Hibernate 预期输出](https://github.com/pingcap-inc/tidb-example-java/blob/main/Expected-Output.md#plain-java-hibernate) +如果在开发的过程中遇到问题,可以在 [AskTUG](https://asktug.com/?utm_source=docs-cn-dev-guide) 上进行提问,寻求帮助。 diff --git a/develop/dev-guide-sample-application-java-jdbc.md b/develop/dev-guide-sample-application-java-jdbc.md index 250943e09693..aae478fa728e 100644 --- a/develop/dev-guide-sample-application-java-jdbc.md +++ b/develop/dev-guide-sample-application-java-jdbc.md @@ -1,576 +1,288 @@ --- -title: TiDB 和 JDBC 的简单 CRUD 应用程序 -summary: 给出一个 TiDB 和 JDBC 的简单 CRUD 应用程序示例。 +title: 使用 JDBC 连接到 TiDB +summary: 了解如何使用 JDBC 连接到 TiDB。本文提供了使用 JDBC 与 TiDB 交互的 Java 示例代码片段。 --- - - +# 使用 JDBC 连接到 TiDB -# TiDB 和 JDBC 的简单 CRUD 应用程序 +TiDB 是一个兼容 MySQL 的数据库。JDBC 是 Java 的数据访问 API。[MySQL Connector/J](https://dev.mysql.com/downloads/connector/j/) 是 MySQL 对 JDBC 的实现。 -本文档将展示如何使用 TiDB 和 JDBC 来构造一个简单的 CRUD 应用程序。 +本文档将展示如何使用 TiDB 和 JDBC 来完成以下任务: -> **注意:** +- 配置你的环境。 +- 使用 JDBC 连接到 TiDB 集群。 +- 构建并运行你的应用程序。你也可以参考[示例代码片段](#示例代码片段),完成基本的 CRUD 操作。 + +> **注意** > -> 推荐使用 Java 8 及以上版本进行 TiDB 的应用程序的编写。 +> 本文档适用于 TiDB Serverless、TiDB Dedicated 和本地部署的 TiDB。 -## 拓展学习视频 +## 前置需求 -- [使用 Connector/J - TiDB v6](https://learn.pingcap.com/learner/course/840002/?utm_source=docs-cn-dev-guide) -- [在 TiDB 上开发应用的最佳实践 - TiDB v6](https://learn.pingcap.com/learner/course/780002/?utm_source=docs-cn-dev-guide) +- 推荐 **Java Development Kit** (JDK) **17** 及以上版本。你可以根据公司及个人需求,自行选择 [OpenJDK](https://openjdk.org/) 或 [Oracle JDK](https://www.oracle.com/hk/java/technologies/downloads/)。 +- [Maven](https://maven.apache.org/install.html) **3.8** 及以上版本。 +- [Git](https://git-scm.com/downloads)。 +- TiDB 集群。如果你还没有 TiDB 集群,可以按照以下方式创建: + - (推荐方式)参考[创建 TiDB Serverless 集群](/develop/dev-guide-build-cluster-in-cloud.md#第-1-步创建-tidb-serverless-集群),创建你自己的 TiDB Cloud 集群。 + - 参考[部署本地测试 TiDB 集群](/quick-start-with-tidb.md#部署本地测试集群)或[部署正式 TiDB 集群](/production-deployment-using-tiup.md),创建本地集群。 -## 第 1 步:启动你的 TiDB 集群 +## 运行代码并连接到 TiDB -本节将介绍 TiDB 集群的启动方法。 +本小节演示如何运行示例应用程序的代码,并连接到 TiDB。 -**使用 TiDB Serverless 集群** +### 第 1 步:克隆示例代码仓库到本地 -详细步骤,请参考:[创建 TiDB Serverless 集群](/develop/dev-guide-build-cluster-in-cloud.md#第-1-步创建-tidb-serverless-集群)。 +运行以下命令,将示例代码仓库克隆到本地: -**使用本地集群** +```bash +git clone https://github.com/tidb-samples/tidb-java-jdbc-quickstart.git +cd tidb-java-jdbc-quickstart +``` -详细步骤,请参考:[部署本地测试 TiDB 集群](/quick-start-with-tidb.md#部署本地测试集群)或[部署正式 TiDB 集群](/production-deployment-using-tiup.md)。 +### 第 2 步:配置连接信息 -## 第 2 步:获取代码 +根据不同的 TiDB 部署方式,使用不同的方法连接到 TiDB 集群。 -```shell -git clone https://github.com/pingcap-inc/tidb-example-java.git -``` + -进入目录 `plain-java-jdbc`: +
-```shell -cd plain-java-jdbc -``` +1. 在 TiDB Cloud 的 [**Clusters**](https://tidbcloud.com/console/clusters) 页面中,选择你的 TiDB Serverless 集群,进入集群的 **Overview** 页面。 -目录结构如下所示: +2. 点击右上角的 **Connect** 按钮,将会弹出连接对话框。 -``` -. -├── Makefile -├── plain-java-jdbc.iml -├── pom.xml -└── src - └── main - ├── java - │ └── com - │ └── pingcap - │ └── JDBCExample.java - └── resources - └── dbinit.sql -``` +3. 确认对话框中的配置和你的运行环境一致。 -其中,`dbinit.sql` 为数据表初始化语句: + - **Endpoint Type** 为 `Public`。 + - **Connect With** 选择 `General`。 + - **Operating System** 为你的运行环境。 -```sql -USE test; -DROP TABLE IF EXISTS player; + > **Tip:** + > + > 如果你在 Windows Subsystem for Linux (WSL) 中运行,请切换为对应的 Linux 发行版。 -CREATE TABLE player ( - `id` VARCHAR(36), - `coins` INTEGER, - `goods` INTEGER, - PRIMARY KEY (`id`) -); -``` +4. 如果你还没有设置密码,点击 **Create password** 生成一个随机密码。 -`JDBCExample.java` 是 `plain-java-jdbc` 这个示例程序的主体。因为 TiDB 与 MySQL 协议兼容,因此,需要初始化一个 MySQL 协议的数据源 `MysqlDataSource`,以此连接到 TiDB。并在其后,初始化 `PlayerDAO`,用来管理数据对象,进行增删改查等操作。 + > **Tip:** + > + > 如果你之前已经生成过密码,可以直接使用原密码,或点击 **Reset password** 重新生成密码。 -`PlayerDAO` 是程序用来管理数据对象的类。其中 `DAO` 是 [Data Access Object](https://en.wikipedia.org/wiki/Data_access_object) 的缩写。在其中定义了一系列数据的操作方法,用来对提供数据的写入能力。 +5. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: -`PlayerBean` 是数据实体类,为数据库表在程序内的映射。`PlayerBean` 的每个属性都对应着 `player` 表的一个字段。 + ```bash + cp env.sh.example env.sh + ``` -```java -package com.pingcap; - -import com.mysql.cj.jdbc.MysqlDataSource; - -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.*; - -/** - * Main class for the basic JDBC example. - **/ -public class JDBCExample -{ - public static class PlayerBean { - private String id; - private Integer coins; - private Integer goods; - - public PlayerBean() { - } +6. 复制并粘贴对应连接字符串至 `env.sh` 中。需更改部分示例结果如下: - public PlayerBean(String id, Integer coins, Integer goods) { - this.id = id; - this.coins = coins; - this.goods = goods; - } + ```shell + export TIDB_HOST='{host}' # e.g. gateway01.ap-northeast-1.prod.aws.tidbcloud.com + export TIDB_PORT='4000' + export TIDB_USER='{user}' # e.g. xxxxxx.root + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='true' + ``` - public String getId() { - return id; - } + 注意替换 `{}` 中的占位符为连接对话框中获得的值。 - public void setId(String id) { - this.id = id; - } + TiDB Serverless 要求使用 TLS (SSL) connection,因此 `USE_SSL` 的值应为 `true`。 - public Integer getCoins() { - return coins; - } +7. 保存 `env.sh` 文件。 - public void setCoins(Integer coins) { - this.coins = coins; - } +
- public Integer getGoods() { - return goods; - } +
- public void setGoods(Integer goods) { - this.goods = goods; - } +1. 在 TiDB Cloud 的 [**Clusters**](https://tidbcloud.com/console/clusters) 页面中,选择你的 TiDB Dedicated 集群,进入集群的 **Overview** 页面。 - @Override - public String toString() { - return String.format(" %-8s => %10s\n %-8s => %10s\n %-8s => %10s\n", - "id", this.id, "coins", this.coins, "goods", this.goods); - } - } +2. 点击右上角的 **Connect** 按钮,将会出现连接对话框。 - /** - * Data access object used by 'ExampleDataSource'. - * Example for CURD and bulk insert. - */ - public static class PlayerDAO { - private final MysqlDataSource ds; - private final Random rand = new Random(); +3. 在对话框中点击 **Allow Access from Anywhere**。 - PlayerDAO(MysqlDataSource ds) { - this.ds = ds; - } + 更多配置细节,可参考 [TiDB Dedicated 标准连接教程(英文)](https://docs.pingcap.com/tidbcloud/connect-via-standard-connection)。 - /** - * Create players by passing in a List of PlayerBean. - * - * @param players Will create players list - * @return The number of create accounts - */ - public int createPlayers(List players){ - int rows = 0; - - Connection connection = null; - PreparedStatement preparedStatement = null; - try { - connection = ds.getConnection(); - preparedStatement = connection.prepareStatement("INSERT INTO player (id, coins, goods) VALUES (?, ?, ?)"); - } catch (SQLException e) { - System.out.printf("[createPlayers] ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - e.printStackTrace(); - - return -1; - } - - try { - for (PlayerBean player : players) { - preparedStatement.setString(1, player.getId()); - preparedStatement.setInt(2, player.getCoins()); - preparedStatement.setInt(3, player.getGoods()); - - preparedStatement.execute(); - rows += preparedStatement.getUpdateCount(); - } - } catch (SQLException e) { - System.out.printf("[createPlayers] ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - e.printStackTrace(); - } finally { - try { - connection.close(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - System.out.printf("\n[createPlayers]:\n '%s'\n", preparedStatement); - return rows; - } +4. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - /** - * 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. - * - * @return The number of effected players. - */ - public int buyGoods(String sellId, String buyId, Integer amount, Integer price) { - int effectPlayers = 0; - - Connection connection = null; - try { - connection = ds.getConnection(); - } catch (SQLException e) { - System.out.printf("[buyGoods] ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - e.printStackTrace(); - return effectPlayers; - } - - try { - connection.setAutoCommit(false); - - PreparedStatement playerQuery = connection.prepareStatement("SELECT * FROM player WHERE id=? OR id=? FOR UPDATE"); - playerQuery.setString(1, sellId); - playerQuery.setString(2, buyId); - playerQuery.execute(); - - PlayerBean sellPlayer = null; - PlayerBean buyPlayer = null; - - ResultSet playerQueryResultSet = playerQuery.getResultSet(); - while (playerQueryResultSet.next()) { - PlayerBean player = new PlayerBean( - playerQueryResultSet.getString("id"), - playerQueryResultSet.getInt("coins"), - playerQueryResultSet.getInt("goods") - ); - - System.out.println("\n[buyGoods]:\n 'check goods and coins enough'"); - System.out.println(player); - - if (sellId.equals(player.getId())) { - sellPlayer = player; - } else { - buyPlayer = player; - } - } - - if (sellPlayer == null || buyPlayer == null) { - throw new SQLException("player not exist."); - } - - if (sellPlayer.getGoods().compareTo(amount) < 0) { - throw new SQLException(String.format("sell player %s goods not enough.", sellId)); - } - - if (buyPlayer.getCoins().compareTo(price) < 0) { - throw new SQLException(String.format("buy player %s coins not enough.", buyId)); - } - - PreparedStatement transfer = connection.prepareStatement("UPDATE player set goods = goods + ?, coins = coins + ? WHERE id=?"); - transfer.setInt(1, -amount); - transfer.setInt(2, price); - transfer.setString(3, sellId); - transfer.execute(); - effectPlayers += transfer.getUpdateCount(); - - transfer.setInt(1, amount); - transfer.setInt(2, -price); - transfer.setString(3, buyId); - transfer.execute(); - effectPlayers += transfer.getUpdateCount(); - - connection.commit(); - - System.out.println("\n[buyGoods]:\n 'trade success'"); - } catch (SQLException e) { - System.out.printf("[buyGoods] ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - - try { - System.out.println("[buyGoods] Rollback"); - - connection.rollback(); - } catch (SQLException ex) { - // do nothing - } - } finally { - try { - connection.close(); - } catch (SQLException e) { - // do nothing - } - } - - return effectPlayers; - } + ```bash + cp env.sh.example env.sh + ``` - /** - * Get the player info by id. - * - * @param id Player id. - * @return The player of this id. - */ - public PlayerBean getPlayer(String id) { - PlayerBean player = null; - - try (Connection connection = ds.getConnection()) { - PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM player WHERE id = ?"); - preparedStatement.setString(1, id); - preparedStatement.execute(); - - ResultSet res = preparedStatement.executeQuery(); - if(!res.next()) { - System.out.printf("No players in the table with id %s", id); - } else { - player = new PlayerBean(res.getString("id"), res.getInt("coins"), res.getInt("goods")); - } - } catch (SQLException e) { - System.out.printf("PlayerDAO.getPlayer ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - } - - return player; - } +5. 复制并粘贴对应的连接字符串至 `env.sh` 中。需更改部分示例结果如下: - /** - * Insert randomized account data (id, coins, goods) using the JDBC fast path for - * bulk inserts. The fastest way to get data into TiDB is using the - * TiDB Lightning(https://docs.pingcap.com/tidb/stable/tidb-lightning-overview). - * However, if you must bulk insert from the application using INSERT SQL, the best - * option is the method shown here. It will require the following: - * - * Add `rewriteBatchedStatements=true` to your JDBC connection settings. - * Setting rewriteBatchedStatements to true now causes CallableStatements - * with batched arguments to be re-written in the form "CALL (...); CALL (...); ..." - * to send the batch in as few client/server round trips as possible. - * https://dev.mysql.com/doc/relnotes/connector-j/5.1/en/news-5-1-3.html - * - * You can see the `rewriteBatchedStatements` param effect logic at - * implement function: `com.mysql.cj.jdbc.StatementImpl.executeBatchUsingMultiQueries` - * - * @param total Add players amount. - * @param batchSize Bulk insert size for per batch. - * - * @return The number of new accounts inserted. - */ - public int bulkInsertRandomPlayers(Integer total, Integer batchSize) { - int totalNewPlayers = 0; - - try (Connection connection = ds.getConnection()) { - // We're managing the commit lifecycle ourselves, so we can - // control the size of our batch inserts. - connection.setAutoCommit(false); - - // In this example we are adding 500 rows to the database, - // but it could be any number. What's important is that - // the batch size is 128. - try (PreparedStatement pstmt = connection.prepareStatement("INSERT INTO player (id, coins, goods) VALUES (?, ?, ?)")) { - for (int i=0; i<=(total/batchSize);i++) { - for (int j=0; j %s row(s) updated in this batch\n", count.length); - } - connection.commit(); - } catch (SQLException e) { - System.out.printf("PlayerDAO.bulkInsertRandomPlayers ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - } - } catch (SQLException e) { - System.out.printf("PlayerDAO.bulkInsertRandomPlayers ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - } - return totalNewPlayers; - } + ```shell + export TIDB_HOST='{host}' # e.g. tidb.xxxx.clusters.tidb-cloud.com + export TIDB_PORT='4000' + export TIDB_USER='{user}' # e.g. root + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='false' + ``` + 注意替换 `{}` 中的占位符为连接对话框中获得的值。 - /** - * Print a subset of players from the data store by limit. - * - * @param limit Print max size. - */ - public void printPlayers(Integer limit) { - try (Connection connection = ds.getConnection()) { - PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM player LIMIT ?"); - preparedStatement.setInt(1, limit); - preparedStatement.execute(); - - ResultSet res = preparedStatement.executeQuery(); - while (!res.next()) { - PlayerBean player = new PlayerBean(res.getString("id"), - res.getInt("coins"), res.getInt("goods")); - System.out.println("\n[printPlayers]:\n" + player); - } - } catch (SQLException e) { - System.out.printf("PlayerDAO.printPlayers ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - } - } +6. 保存 `env.sh` 文件。 +
- /** - * Count players from the data store. - * - * @return All players count - */ - public int countPlayers() { - int count = 0; - - try (Connection connection = ds.getConnection()) { - PreparedStatement preparedStatement = connection.prepareStatement("SELECT count(*) FROM player"); - preparedStatement.execute(); - - ResultSet res = preparedStatement.executeQuery(); - if(res.next()) { - count = res.getInt(1); - } - } catch (SQLException e) { - System.out.printf("PlayerDAO.countPlayers ERROR: { state => %s, cause => %s, message => %s }\n", - e.getSQLState(), e.getCause(), e.getMessage()); - } - - return count; - } - } +
+ +1. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - public static void main(String[] args) { - // 1. Configure the example database connection. + ```bash + cp env.sh.example env.sh + ``` - // 1.1 Create a mysql data source instance. - MysqlDataSource mysqlDataSource = new MysqlDataSource(); +2. 复制并粘贴对应 TiDB 的连接字符串至 `env.sh` 中。需更改部分示例结果如下: - // 1.2 Set server name, port, database name, username and password. - mysqlDataSource.setServerName("localhost"); - mysqlDataSource.setPortNumber(4000); - mysqlDataSource.setDatabaseName("test"); - mysqlDataSource.setUser("root"); - mysqlDataSource.setPassword(""); + ```shell + export TIDB_HOST='{host}' + export TIDB_PORT='4000' + export TIDB_USER='root' + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='false' + ``` - // Or you can use jdbc string instead. - // mysqlDataSource.setURL("jdbc:mysql://{host}:{port}/test?user={user}&password={password}"); + 注意替换 `{}` 中的占位符为你的 TiDB 对应的值,并设置 `USE_SSL` 为 `false`。如果你在本机运行 TiDB,默认 Host 地址为 `127.0.0.1`,密码为空。 - // 2. And then, create DAO to manager your data. - PlayerDAO dao = new PlayerDAO(mysqlDataSource); +3. 保存 `env.sh` 文件。 - // 3. Run some simple examples. +
- // Create a player, who has a coin and a goods.. - dao.createPlayers(Collections.singletonList(new PlayerBean("test", 1, 1))); +
- // Get a player. - PlayerBean testPlayer = dao.getPlayer("test"); - System.out.printf("PlayerDAO.getPlayer:\n => id: %s\n => coins: %s\n => goods: %s\n", - testPlayer.getId(), testPlayer.getCoins(), testPlayer.getGoods()); +### 第 3 步:运行代码并查看结果 - // Create players with bulk inserts. Insert 1919 players totally, with 114 players per batch. - int addedCount = dao.bulkInsertRandomPlayers(1919, 114); - System.out.printf("PlayerDAO.bulkInsertRandomPlayers:\n => %d total inserted players\n", addedCount); +1. 运行下述命令,执行示例代码: - // Count players amount. - int count = dao.countPlayers(); - System.out.printf("PlayerDAO.countPlayers:\n => %d total players\n", count); + ```shell + make + ``` - // Print 3 players. - dao.printPlayers(3); +2. 查看 [`Expected-Output.txt`](https://github.com/tidb-samples/tidb-java-jdbc-quickstart/blob/main/Expected-Output.txt),并与你的程序输出进行比较。结果近似即为连接成功。 - // 4. Explore more. +## 示例代码片段 - // Player 1: id is "1", has only 100 coins. - // Player 2: id is "2", has 114514 coins, and 20 goods. - PlayerBean player1 = new PlayerBean("1", 100, 0); - PlayerBean player2 = new PlayerBean("2", 114514, 20); +你可参考以下关键代码片段,完成自己的应用开发。 - // Create two players "by hand", using the INSERT statement on the backend. - addedCount = dao.createPlayers(Arrays.asList(player1, player2)); - System.out.printf("PlayerDAO.createPlayers:\n => %d total inserted players\n", addedCount); +完整代码及其运行方式,见代码仓库 [tidb-java-jdbc-quickstart](https://github.com/tidb-samples/tidb-java-jdbc-quickstart/blob/main/README-zh.md)。 - // Player 1 wants to buy 10 goods from player 2. - // It will cost 500 coins, but player 1 cannot afford it. - System.out.println("\nPlayerDAO.buyGoods:\n => this trade will fail"); - int updatedCount = dao.buyGoods(player2.getId(), player1.getId(), 10, 500); - System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount); +### 连接到 TiDB - // So player 1 has to reduce the incoming quantity to two. - System.out.println("\nPlayerDAO.buyGoods:\n => this trade will success"); - updatedCount = dao.buyGoods(player2.getId(), player1.getId(), 2, 100); - System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount); +```java +public MysqlDataSource getMysqlDataSource() throws SQLException { + MysqlDataSource mysqlDataSource = new MysqlDataSource(); + + mysqlDataSource.setServerName(${tidb_host}); + mysqlDataSource.setPortNumber(${tidb_port}); + mysqlDataSource.setUser(${tidb_user}); + mysqlDataSource.setPassword(${tidb_password}); + mysqlDataSource.setDatabaseName(${tidb_db_name}); + if (${tidb_use_ssl}) { + mysqlDataSource.setSslMode(PropertyDefinitions.SslMode.VERIFY_IDENTITY.name()); + mysqlDataSource.setEnabledTLSProtocols("TLSv1.2,TLSv1.3"); } + + return mysqlDataSource; } ``` -## 第 3 步:运行代码 +在使用该函数时,你需要将 `${tidb_host}`、`${tidb_port}`、`${tidb_user}`、`${tidb_password}`、`${tidb_db_name}` 等替换为你的 TiDB 集群的实际值。 -本节将逐步介绍代码的运行方法。 +### 插入数据 -### 第 3 步第 1 部分:JDBC 表初始化 - -使用 JDBC 时,需手动初始化数据库表,若你本地已经安装了 `mysql-client`,且使用本地集群,可直接在 `plain-java-jdbc` 目录下运行: - -```shell -make mysql +```java +public void createPlayer(PlayerBean player) throws SQLException { + MysqlDataSource mysqlDataSource = getMysqlDataSource(); + try (Connection connection = mysqlDataSource.getConnection()) { + PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO player (id, coins, goods) VALUES (?, ?, ?)"); + preparedStatement.setString(1, player.getId()); + preparedStatement.setInt(2, player.getCoins()); + preparedStatement.setInt(3, player.getGoods()); + + preparedStatement.execute(); + } +} ``` -或直接执行: +更多信息参考[插入数据](/develop/dev-guide-insert-data.md)。 -```shell -mysql --host 127.0.0.1 --port 4000 -u root - +# 使用 MyBatis 连接到 TiDB -# TiDB 和 MyBatis 的简单 CRUD 应用程序 +TiDB 是一个兼容 MySQL 的数据库。[MyBatis](https://mybatis.org/mybatis-3/index.html) 是当前比较流行的开源 Java 应用持久层框架。 -[Mybatis](https://mybatis.org/mybatis-3/index.html) 是当前比较流行的开源 Java 应用持久层框架。 +本文档将展示如何使用 TiDB 和 MyBatis 来完成以下任务: -本文档将展示如何使用 TiDB 和 MyBatis 来构造一个简单的 CRUD 应用程序。 +- 配置你的环境。 +- 使用 MyBatis 连接到 TiDB 集群。 +- 构建并运行你的应用程序。你也可以参考[示例代码片段](#示例代码片段),完成基本的 CRUD 操作。 -> **注意:** +> **注意** > -> 推荐使用 Java 8 及以上版本进行 TiDB 的应用程序的编写。 +> 本文档适用于 TiDB Serverless、TiDB Dedicated 和本地部署的 TiDB。 -## 拓展学习视频 +## 前置需求 -- [使用 Connector/J - TiDB v6](https://learn.pingcap.com/learner/course/840002/?utm_source=docs-cn-dev-guide) -- [在 TiDB 上开发应用的最佳实践 - TiDB v6](https://learn.pingcap.com/learner/course/780002/?utm_source=docs-cn-dev-guide) +- 推荐 **Java Development Kit** (JDK) **17** 及以上版本。你可以根据公司及个人需求,自行选择 [OpenJDK](https://openjdk.org/) 或 [Oracle JDK](https://www.oracle.com/hk/java/technologies/downloads/)。 +- [Maven](https://maven.apache.org/install.html) **3.8** 及以上版本。 +- [Git](https://git-scm.com/downloads)。 +- TiDB 集群。如果你还没有 TiDB 集群,可以按照以下方式创建: + - (推荐方式)参考[创建 TiDB Serverless 集群](/develop/dev-guide-build-cluster-in-cloud.md#第-1-步创建-tidb-serverless-集群),创建你自己的 TiDB Cloud 集群。 + - 参考[部署本地测试 TiDB 集群](/quick-start-with-tidb.md#部署本地测试集群)或[部署正式 TiDB 集群](/production-deployment-using-tiup.md),创建本地集群。 -## 第 1 步:启动你的 TiDB 集群 +## 运行代码并连接到 TiDB -本节将介绍 TiDB 集群的启动方法。 +本小节演示如何运行示例应用程序的代码,并连接到 TiDB。 -**使用 TiDB Serverless 集群** +### 第 1 步:克隆示例代码仓库到本地 -详细步骤,请参考:[创建 TiDB Serverless 集群](/develop/dev-guide-build-cluster-in-cloud.md#第-1-步创建-tidb-serverless-集群)。 +运行以下命令,将示例代码仓库克隆到本地: -**使用本地集群** - -详细步骤,请参考:[部署本地测试 TiDB 集群](/quick-start-with-tidb.md#部署本地测试集群)或[部署正式 TiDB 集群](/production-deployment-using-tiup.md)。 - -## 第 2 步:获取代码 - -```shell -git clone https://github.com/pingcap-inc/tidb-example-java.git +```bash +git clone https://github.com/tidb-samples/tidb-java-mybatis-quickstart.git +cd tidb-java-mybatis-quickstart ``` -与 [MyBatis](https://mybatis.org/mybatis-3/index.html) 对比,JDBC 的实现方式并非最优体验。你需要自行编写错误处理逻辑,并且代码无法简单复用。这会使你的代码有些冗余。 - -本文将以 Maven 插件的方式使用 [MyBatis Generator](https://mybatis.org/generator/quickstart.html) 生成部分持久层代码。 +### 第 2 步:配置连接信息 -进入目录 `plain-java-mybatis`: +根据不同的 TiDB 部署方式,使用不同的方法连接到 TiDB 集群。 -```shell -cd plain-java-mybatis -``` + -目录结构如下所示: +
-``` -. -├── Makefile -├── pom.xml -└── src - └── main - ├── java - │   └── com - │   └── pingcap - │   ├── MybatisExample.java - │   ├── dao - │   │   └── PlayerDAO.java - │   └── model - │   ├── Player.java - │   ├── PlayerMapper.java - │   └── PlayerMapperEx.java - └── resources - ├── dbinit.sql - ├── log4j.properties - ├── mapper - │   ├── PlayerMapper.xml - │   └── PlayerMapperEx.xml - ├── mybatis-config.xml - └── mybatis-generator.xml -``` +1. 在 TiDB Cloud 的 [**Clusters**](https://tidbcloud.com/console/clusters) 页面中,选择你的 TiDB Serverless 集群,进入集群的 **Overview** 页面。 -其中,自动生成的文件有: +2. 点击右上角的 **Connect** 按钮,将会弹出连接对话框。 -- `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 接口的实现类的配置 +3. 确认对话框中的配置和你的运行环境一致。 -这些文件的生成策略被写在了 `mybatis-generator.xml` 配置文件内,它是 [MyBatis Generator](https://mybatis.org/generator/quickstart.html) 的配置文件,下面配置文件中添加了使用方法的说明: + - **Endpoint Type** 为 `Public`。 + - **Connect With** 选择 `General`。 + - **Operating System** 为你的运行环境。 -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` + > **Tip:** + > + > 如果你在 Windows Subsystem for Linux (WSL) 中运行,请切换为对应的 Linux 发行版。 -`mybatis-generator.xml` 在 `pom.xml` 中,以 `mybatis-generator-maven-plugin` 插件配置的方式被引入: +4. 如果你还没有设置密码,点击 **Create password** 生成一个随机密码。 -```xml - - org.mybatis.generator - mybatis-generator-maven-plugin - 1.4.1 - - src/main/resources/mybatis-generator.xml - true - true - - - - - - mysql - mysql-connector-java - 5.1.49 - - - -``` + > **Tip:** + > + > 如果你之前已经生成过密码,可以直接使用原密码,或点击 **Reset password** 重新生成密码。 -在 Maven 插件内引入后,可删除旧的生成文件后,通过命令 `mvn mybatis-generate` 生成新的文件。或者你也可以使用已经编写好的 `make` 命令,通过 `make gen` 来同时删除旧文件,并生成新文件。 +5. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: -> **注意:** -> -> `mybatis-generator.xml` 中的属性 `configuration.overwrite` 仅可控制新生成的 Java 代码文件使用覆盖方式被写入,但 XML 映射文件仍会以追加方式写入。因此,推荐在 MyBatis Generator 生成新的文件前,先删除掉旧的文件。 + ```bash + cp env.sh.example env.sh + ``` -`Player.java` 是使用 MyBatis Generator 生成出的数据实体类文件,为数据库表在程序内的映射。`Player` 类的每个属性都对应着 `player` 表的一个字段。 +6. 复制并粘贴对应连接字符串至 `env.sh` 中。需更改部分示例结果如下: -```java -package com.pingcap.model; + ```shell + export TIDB_HOST='{host}' # e.g. gateway01.ap-northeast-1.prod.aws.tidbcloud.com + export TIDB_PORT='4000' + export TIDB_USER='{user}' # e.g. xxxxxx.root + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='true' + ``` -public class Player { - private String id; + 注意替换 `{}` 中的占位符为连接对话框中获得的值。 - private Integer coins; + TiDB Serverless 要求使用 TLS (SSL) connection,因此 `USE_SSL` 的值应为 `true`。 - private Integer goods; +7. 保存 `env.sh` 文件。 - public Player(String id, Integer coins, Integer goods) { - this.id = id; - this.coins = coins; - this.goods = goods; - } + - public Player() { - super(); - } +
- public String getId() { - return id; - } +1. 在 TiDB Cloud 的 [**Clusters**](https://tidbcloud.com/console/clusters) 页面中,选择你的 TiDB Dedicated 集群,进入集群的 **Overview** 页面。 - public void setId(String id) { - this.id = id; - } +2. 点击右上角的 **Connect** 按钮,将会出现连接对话框。 - public Integer getCoins() { - return coins; - } +3. 在对话框中点击 **Allow Access from Anywhere**。 - public void setCoins(Integer coins) { - this.coins = coins; - } + 更多配置细节,可参考 [TiDB Dedicated 标准连接教程(英文)](https://docs.pingcap.com/tidbcloud/connect-via-standard-connection)。 - public Integer getGoods() { - return goods; - } +4. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - public void setGoods(Integer goods) { - this.goods = goods; - } -} -``` + ```bash + cp env.sh.example env.sh + ``` -`PlayerMapper.java` 是使用 MyBatis Generator 生成出的映射接口文件,它仅规定了接口,接口的实现类是由 MyBatis 来通过 XML 或注解自动生成的: +5. 复制并粘贴对应的连接字符串至 `env.sh` 中。需更改部分示例结果如下: -```java -package com.pingcap.model; + ```shell + export TIDB_HOST='{host}' # e.g. tidb.xxxx.clusters.tidb-cloud.com + export TIDB_PORT='4000' + export TIDB_USER='{user}' # e.g. root + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='false' + ``` -import com.pingcap.model.Player; + 注意替换 `{}` 中的占位符为连接对话框中获得的值。 -public interface PlayerMapper { - int deleteByPrimaryKey(String id); +6. 保存 `env.sh` 文件。 - int insert(Player row); +
- int insertSelective(Player row); +
- Player selectByPrimaryKey(String id); +1. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - int updateByPrimaryKeySelective(Player row); + ```bash + cp env.sh.example env.sh + ``` - int updateByPrimaryKey(Player row); -} -``` - -`PlayerMapper.xml` 是使用 MyBatis Generator 生成出的映射 XML 文件,MyBatis 将使用这个文件自动生成 `PlayerMapper` 接口的实现类: - -```xml - - - - - - - - - - - - id, coins, goods - - - - delete from player - where id = #{id,jdbcType=VARCHAR} - - - insert into player (id, coins, goods - ) - values (#{id,jdbcType=VARCHAR}, #{coins,jdbcType=INTEGER}, #{goods,jdbcType=INTEGER} - ) - - - insert into player - - - id, - - - coins, - - - goods, - - - - - #{id,jdbcType=VARCHAR}, - - - #{coins,jdbcType=INTEGER}, - - - #{goods,jdbcType=INTEGER}, - - - - - update player - - - coins = #{coins,jdbcType=INTEGER}, - - - goods = #{goods,jdbcType=INTEGER}, - - - where id = #{id,jdbcType=VARCHAR} - - - update player - set coins = #{coins,jdbcType=INTEGER}, - goods = #{goods,jdbcType=INTEGER} - where id = #{id,jdbcType=VARCHAR} - - -``` - -由于 MyBatis Generator 需要逆向生成源码,因此,数据库中需先行有此表结构,可使用 `dbinit.sql` 生成表结构: - -```sql -USE test; -DROP TABLE IF EXISTS player; - -CREATE TABLE player ( - `id` VARCHAR(36), - `coins` INTEGER, - `goods` INTEGER, - PRIMARY KEY (`id`) -); -``` - -额外拆分接口 `PlayerMapperEx` 继承 `PlayerMapper`,并且编写与之匹配的 `PlayerMapperEx.xml`。避免直接更改 `PlayerMapper.java` 和 `PlayerMapper.xml`。这是为了规避 MyBatis Generator 的反复生成,影响到自行编写的代码。 - -在 `PlayerMapperEx.java` 中定义自行增加的接口: - -```java -package com.pingcap.model; - -import java.util.List; - -public interface PlayerMapperEx extends PlayerMapper { - Player selectByPrimaryKeyWithLock(String id); - - List selectByLimit(Integer limit); - - Integer count(); -} -``` - -在 `PlayerMapperEx.xml` 中定义映射规则: - -```xml - - - - - - - - - - - - id, coins, goods - - - - - - - - - -``` +2. 复制并粘贴对应 TiDB 的连接字符串至 `env.sh` 中。需更改部分示例结果如下: -`PlayerDAO.java` 是程序用来管理数据对象的类。其中 `DAO` 是 [Data Access Object](https://en.wikipedia.org/wiki/Data_access_object) 的缩写。在其中定义了一系列数据的操作方法,用于数据的写入。 + ```shell + export TIDB_HOST='{host}' + export TIDB_PORT='4000' + export TIDB_USER='root' + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='false' + ``` -```java -package com.pingcap.dao; - -import com.pingcap.model.Player; -import com.pingcap.model.PlayerMapperEx; -import org.apache.ibatis.session.SqlSession; -import org.apache.ibatis.session.SqlSessionFactory; - -import java.util.List; -import java.util.function.Function; - -public class PlayerDAO { - public static class NotEnoughException extends RuntimeException { - public NotEnoughException(String message) { - super(message); - } - } - - // Run SQL code in a way that automatically handles the - // transaction retry logic, so we don't have to duplicate it in - // various places. - public Object runTransaction(SqlSessionFactory sessionFactory, Function fn) { - Object resultObject = null; - SqlSession session = null; - - try { - // open a session with autoCommit is false - session = sessionFactory.openSession(false); - - // get player mapper - PlayerMapperEx playerMapperEx = session.getMapper(PlayerMapperEx.class); - - resultObject = fn.apply(playerMapperEx); - session.commit(); - System.out.println("APP: COMMIT;"); - } catch (Exception e) { - if (e instanceof NotEnoughException) { - System.out.printf("APP: ROLLBACK BY LOGIC; \n%s\n", e.getMessage()); - } else { - System.out.printf("APP: ROLLBACK BY ERROR; \n%s\n", e.getMessage()); - } - - if (session != null) { - session.rollback(); - } - } finally { - if (session != null) { - session.close(); - } - } - - return resultObject; - } - - public Function createPlayers(List players) { - return playerMapperEx -> { - Integer addedPlayerAmount = 0; - for (Player player: players) { - playerMapperEx.insert(player); - addedPlayerAmount ++; - } - System.out.printf("APP: createPlayers() --> %d\n", addedPlayerAmount); - return addedPlayerAmount; - }; - } - - public Function buyGoods(String sellId, String buyId, Integer amount, Integer price) { - return playerMapperEx -> { - Player sellPlayer = playerMapperEx.selectByPrimaryKeyWithLock(sellId); - Player buyPlayer = playerMapperEx.selectByPrimaryKeyWithLock(buyId); - - if (buyPlayer == null || sellPlayer == null) { - throw new NotEnoughException("sell or buy player not exist"); - } - - if (buyPlayer.getCoins() < price || sellPlayer.getGoods() < amount) { - throw new NotEnoughException("coins or goods not enough, rollback"); - } - - int affectRows = 0; - buyPlayer.setGoods(buyPlayer.getGoods() + amount); - buyPlayer.setCoins(buyPlayer.getCoins() - price); - affectRows += playerMapperEx.updateByPrimaryKey(buyPlayer); - - sellPlayer.setGoods(sellPlayer.getGoods() - amount); - sellPlayer.setCoins(sellPlayer.getCoins() + price); - affectRows += playerMapperEx.updateByPrimaryKey(sellPlayer); - - System.out.printf("APP: buyGoods --> sell: %s, buy: %s, amount: %d, price: %d\n", sellId, buyId, amount, price); - return affectRows; - }; - } - - public Function getPlayerByID(String id) { - return playerMapperEx -> playerMapperEx.selectByPrimaryKey(id); - } - - public Function printPlayers(Integer limit) { - return playerMapperEx -> { - List players = playerMapperEx.selectByLimit(limit); - - for (Player player: players) { - System.out.println("\n[printPlayers]:\n" + player); - } - return 0; - }; - } - - public Function countPlayers() { - return PlayerMapperEx::count; - } -} -``` + 注意替换 `{}` 中的占位符为你的 TiDB 对应的值,并设置 `USE_SSL` 为 `false`。如果你在本机运行 TiDB,默认 Host 地址为 `127.0.0.1`,密码为空。 -`MybatisExample` 是 `plain-java-mybatis` 这个示例程序的主类。其中定义了入口函数: +3. 保存 `env.sh` 文件。 -```java -package com.pingcap; - -import com.pingcap.dao.PlayerDAO; -import com.pingcap.model.Player; -import org.apache.ibatis.io.Resources; -import org.apache.ibatis.session.SqlSessionFactory; -import org.apache.ibatis.session.SqlSessionFactoryBuilder; - -import java.io.IOException; -import java.io.InputStream; -import java.util.Arrays; -import java.util.Collections; - -public class MybatisExample { - public static void main( String[] args ) throws IOException { - // 1. Create a SqlSessionFactory based on our mybatis-config.xml configuration - // file, which defines how to connect to the database. - InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); - SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream); - - // 2. And then, create DAO to manager your data - PlayerDAO playerDAO = new PlayerDAO(); - - // 3. Run some simple examples. - - // Create a player who has 1 coin and 1 goods. - playerDAO.runTransaction(sessionFactory, playerDAO.createPlayers( - Collections.singletonList(new Player("test", 1, 1)))); - - // Get a player. - Player testPlayer = (Player)playerDAO.runTransaction(sessionFactory, playerDAO.getPlayerByID("test")); - System.out.printf("PlayerDAO.getPlayer:\n => id: %s\n => coins: %s\n => goods: %s\n", - testPlayer.getId(), testPlayer.getCoins(), testPlayer.getGoods()); - - // Count players amount. - Integer count = (Integer)playerDAO.runTransaction(sessionFactory, playerDAO.countPlayers()); - System.out.printf("PlayerDAO.countPlayers:\n => %d total players\n", count); - - // Print 3 players. - playerDAO.runTransaction(sessionFactory, playerDAO.printPlayers(3)); - - // 4. Getting further. - - // Player 1: id is "1", has only 100 coins. - // Player 2: id is "2", has 114514 coins, and 20 goods. - Player player1 = new Player("1", 100, 0); - Player player2 = new Player("2", 114514, 20); - - // Create two players "by hand", using the INSERT statement on the backend. - int addedCount = (Integer)playerDAO.runTransaction(sessionFactory, - playerDAO.createPlayers(Arrays.asList(player1, player2))); - System.out.printf("PlayerDAO.createPlayers:\n => %d total inserted players\n", addedCount); - - // Player 1 wants to buy 10 goods from player 2. - // It will cost 500 coins, but player 1 cannot afford it. - System.out.println("\nPlayerDAO.buyGoods:\n => this trade will fail"); - Integer updatedCount = (Integer)playerDAO.runTransaction(sessionFactory, - playerDAO.buyGoods(player2.getId(), player1.getId(), 10, 500)); - System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount); - - // So player 1 has to reduce the incoming quantity to two. - System.out.println("\nPlayerDAO.buyGoods:\n => this trade will success"); - updatedCount = (Integer)playerDAO.runTransaction(sessionFactory, - playerDAO.buyGoods(player2.getId(), player1.getId(), 2, 100)); - System.out.printf("PlayerDAO.buyGoods:\n => %d total update players\n", updatedCount); - } -} -``` +
-## 第 3 步:运行代码 + -本节将逐步介绍代码的运行方法。 +### 第 3 步:运行代码并查看结果 -### 第 3 步第 1 部分:表初始化 +1. 运行下述命令,执行示例代码: -使用 MyBatis 时,需手动初始化数据库表。若你本地已经安装了 `mysql-client`,且使用本地集群,可直接在 `plain-java-mybatis` 目录下通过 `make prepare` 运行: + ```shell + make + ``` -```shell -make prepare -``` +2. 查看 [`Expected-Output.txt`](https://github.com/tidb-samples/tidb-java-mybatis-quickstart/blob/main/Expected-Output.txt),并与你的程序输出进行比较。结果近似即为连接成功。 -或直接执行: +## 示例代码片段 -```shell -mysql --host 127.0.0.1 --port 4000 -u root < src/main/resources/dbinit.sql -``` +你可参考以下关键代码片段,完成自己的应用开发。 -若你不使用本地集群,或未安装 `mysql-client`,请直接登录你的集群,并运行 `src/main/resources/dbinit.sql` 文件内的 SQL 语句。 +完整代码及其运行方式,见代码仓库 [tidb-java-mybatis-quickstart](https://github.com/tidb-samples/tidb-java-mybatis-quickstart/blob/main/README-zh.md)。 -### 第 3 步第 2 部分:TiDB Cloud 更改参数 +### 连接到 TiDB -若你使用 TiDB Serverless 集群,更改 `mybatis-config.xml` 内关于 `dataSource.url`、`dataSource.username`、`dataSource.password` 的参数: +编写配置文件 `mybatis-config.xml`: ```xml - - @@ -643,96 +185,120 @@ mysql --host 127.0.0.1 --port 4000 -u root < src/main/resources/dbinit.sql - - - - - - - - + + + + - - - + - ``` -若你设定的密码为 `123456`,而且从 TiDB Serverless 集群面板中得到的连接信息为: +请将 `${tidb_jdbc_url}`、`${tidb_user}`、`${tidb_password}` 等替换为你的 TiDB 集群的实际值。并替换 `${mapper_location}` 的值为你的 mapper XML 配置文件的位置。如果你有多个 mapper XML 配置文件,需要添加多个 `` 标签。随后编写以下函数: -- Endpoint: `xxx.tidbcloud.com` -- Port: `4000` -- User: `2aEp24QWEDLqRFs.root` +```java +public SqlSessionFactory getSessionFactory() { + InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); + SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream); +} +``` + +### 插入数据 -那么此处应将配置文件中 `dataSource` 节点内更改为: +在 mapper XML 中添加节点,并在 XML 配置文件的 `mapper.namespace` 属性中配置的接口类中添加同名函数: ```xml - + + + + + insert into player (id, coins, goods) + values (#{id,jdbcType=VARCHAR}, #{coins,jdbcType=INTEGER}, #{goods,jdbcType=INTEGER}) + + +``` - +更多信息参考[插入数据](/develop/dev-guide-insert-data.md)。 - ... - - - - - - - - ... +### 查询数据 - -``` +在 mapper XML 中添加节点,并在 XML 配置文件的 `mapper.namespace` 属性中配置的接口类中添加同名函数。特别地,如果你在 MyBatis 的查询函数中使用 `resultMap` 作为返回类型,需要额外注意配置文件的 `` 节点配置是否正确。 -### 第 3 步第 3 部分:运行 +```xml + + + + + + + + + + + + + +``` -你可以分别运行 `make prepare`, `make gen`, `make build` 和 `make run` 以运行此代码: +更多信息参考[查询数据](/develop/dev-guide-get-data-from-single-table.md)。 -```shell -make prepare -# this command executes : -# - `mysql --host 127.0.0.1 --port 4000 -u root < src/main/resources/dbinit.sql` -# - `mysql --host 127.0.0.1 --port 4000 -u root -e "TRUNCATE test.player"` +### 更新数据 -make gen -# this command executes : -# - `rm -f src/main/java/com/pingcap/model/Player.java` -# - `rm -f src/main/java/com/pingcap/model/PlayerMapper.java` -# - `rm -f src/main/resources/mapper/PlayerMapper.xml` -# - `mvn mybatis-generator:generate` +在 mapper XML 中添加节点,并在 XML 配置文件的 `mapper.namespace` 属性中配置的接口类中添加同名函数: -make build # this command executes `mvn clean package` -make run # this command executes `java -jar target/plain-java-mybatis-0.0.1-jar-with-dependencies.jar` +```xml + + + + + update player + set coins = #{coins,jdbcType=INTEGER}, + goods = #{goods,jdbcType=INTEGER} + where id = #{id,jdbcType=VARCHAR} + + ``` -或者你也可以直接使用原生的命令: - -```shell -mysql --host 127.0.0.1 --port 4000 -u root < src/main/resources/dbinit.sql -mysql --host 127.0.0.1 --port 4000 -u root -e "TRUNCATE test.player" -rm -f src/main/java/com/pingcap/model/Player.java -rm -f src/main/java/com/pingcap/model/PlayerMapper.java -rm -f src/main/resources/mapper/PlayerMapper.xml -mvn mybatis-generator:generate -mvn clean package -java -jar target/plain-java-mybatis-0.0.1-jar-with-dependencies.jar +更多信息参考[更新数据](/develop/dev-guide-update-data.md)。 + +### 删除数据 + +在 mapper XML 中添加节点,并在 XML 配置文件的 `mapper.namespace` 属性中配置的接口类中添加同名函数: + +```xml + + + + + delete from player + where id = #{id,jdbcType=VARCHAR} + + ``` -再或者直接运行 `make` 命令,这是 `make prepare`, `make gen`, `make build` 和 `make run` 的组合。 +更多信息参考[删除数据](/develop/dev-guide-delete-data.md)。 + +## 下一步 + +- 关于 MyBatis 的更多使用方法,可以参考 [MyBatis 官方文档](http://www.mybatis.org/mybatis-3/)。 +- 你可以继续阅读开发者文档,以获取更多关于 TiDB 应用开发的最佳实践。例如:[插入数据](/develop/dev-guide-insert-data.md)、[更新数据](/develop/dev-guide-update-data.md)、[删除数据](/develop/dev-guide-delete-data.md)、[单表读取](/develop/dev-guide-get-data-from-single-table.md)、[事务](/develop/dev-guide-transaction-overview.md)、[SQL 性能优化](/develop/dev-guide-optimize-sql-overview.md)等。 +- 如果你更倾向于参与课程进行学习,我们也提供专业的 [TiDB 开发者课程](https://cn.pingcap.com/courses-catalog/back-end-developer/?utm_source=docs-cn-dev-guide)支持,并在考试后提供相应的[资格认证](https://learn.pingcap.com/learner/certification-center)。 +- 我们还额外提供针对 Java 开发者的课程:[使用 Connector/J - TiDB v6](https://learn.pingcap.com/learner/course/840002/?utm_source=docs-cn-dev-guide) 及[在 TiDB 上开发应用的最佳实践 - TiDB v6](https://learn.pingcap.com/learner/course/780002/?utm_source=docs-cn-dev-guide)。 -## 第 4 步:预期输出 +## 需要帮助? -[MyBatis 预期输出](https://github.com/pingcap-inc/tidb-example-java/blob/main/Expected-Output.md#plain-java-mybatis) +如果在开发的过程中遇到问题,可以在 [AskTUG](https://asktug.com/?utm_source=docs-cn-dev-guide) 上进行提问,寻求帮助。 diff --git a/develop/dev-guide-sample-application-java-spring-boot.md b/develop/dev-guide-sample-application-java-spring-boot.md index f823f4e808d1..eba7811eb443 100644 --- a/develop/dev-guide-sample-application-java-spring-boot.md +++ b/develop/dev-guide-sample-application-java-spring-boot.md @@ -1,575 +1,190 @@ --- -title: 使用 Spring Boot 构建 TiDB 应用程序 -summary: 给出一个 Spring Boot 构建 TiDB 应用程序示例。 +title: 使用 Spring Boot 连接到 TiDB +summary: 了解如何使用 Spring Boot 连接到 TiDB。本文提供了使用 Spring Boot 与 TiDB 交互的 Java 示例代码片段。 aliases: ['/zh/tidb/dev/dev-guide-sample-application-spring-boot', '/zh/tidb/dev/sample-application-spring-boot'] --- - +# 使用 Spring Boot 连接到 TiDB -# 使用 Spring Boot 构建 TiDB 应用程序 +TiDB 是一个兼容 MySQL 的数据库。[Spring](https://spring.io/) 是当前比较流行的开源 Java 容器框架,本文选择 [Spring Boot](https://spring.io/projects/spring-boot) 作为使用 Spring 的方式。 -本教程向你展示如何使用 TiDB 构建 [Spring Boot](https://spring.io/projects/spring-boot) Web 应用程序。使用 [Spring Data JPA](https://spring.io/projects/spring-data-jpa) 模块作为数据访问能力的框架。此示例应用程序的代码仓库可在 [Github](https://github.com/pingcap-inc/tidb-example-java) 下载。 +本文档将展示如何使用 TiDB 和 [Spring Data JPA](https://spring.io/projects/spring-data-jpa) 及 [Hibernate](https://hibernate.org/orm/) 作为 JPA 提供者来完成以下任务: -这是一个较为完整的构建 Restful API 的示例应用程序,展示了一个使用 **TiDB** 作为数据库的通用 **Spring Boot** 后端服务。设计了以下过程,用于还原一个现实场景: +- 配置你的环境。 +- 使用 Spring Data JPA 与 Hibernate 连接到 TiDB 集群。 +- 构建并运行你的应用程序。你也可以参考[示例代码片段](#示例代码片段),完成基本的 CRUD 操作。 -这是一个关于游戏的例子,每个玩家有两个属性:金币数 `coins` 和货物数 `goods`。且每个玩家都拥有一个字段 `id`,作为玩家的唯一标识。玩家在金币数和货物数充足的情况下,可以自由的交易。 - -你可以以此示例为基础,构建自己的应用程序。 - -## 第 1 步:启动你的 TiDB 集群 - -本节将介绍 TiDB 集群的启动方法。 - -**使用 TiDB Serverless 集群** - -详细步骤,请参考:[创建 TiDB Serverless 集群](/develop/dev-guide-build-cluster-in-cloud.md#第-1-步创建-tidb-serverless-集群)。 - -**使用本地集群** - -详细步骤,请参考:[部署本地测试 TiDB 集群](/quick-start-with-tidb.md#部署本地测试集群)或[部署正式 TiDB 集群](/production-deployment-using-tiup.md)。 - -## 第 2 步:安装 JDK - -请在你的计算机上下载并安装 **Java Development Kit** (JDK),这是 Java 开发的必备工具。**Spring Boot** 支持 Java 版本 8 以上的 JDK,由于 **Hibernate** 版本的缘故,推荐使用 Java 版本 11 以上的 JDK。 - -示例应用程序同时支持 **Oracle JDK** 和 **OpenJDK**,请自行选择,本教程将使用版本 17 的 **OpenJDK**。 - -## 第 3 步:安装 Maven - -此示例应用程序使用 **Maven** 来管理应用程序的依赖项。Spring 支持的 **Maven** 版本为 3.2 以上,作为依赖管理软件,推荐使用当前最新稳定版本的 **Maven**。 - -这里给出命令行安装 **Maven** 的办法: - -- macOS 安装: - - {{< copyable "shell-regular" >}} - - ``` - brew install maven - ``` - -- 基于 Debian 的 Linux 发行版上安装(如 Ubuntu 等): - - {{< copyable "shell-regular" >}} - - ``` - apt-get install maven - ``` - -- 基于 Red Hat 的 Linux 发行版上安装(如 Fedora、CentOS 等): - -- dnf 包管理器 - - {{< copyable "shell-regular" >}} - - ``` - dnf install maven - ``` - -- yum 包管理器 - - {{< copyable "shell-regular" >}} - - ``` - yum install maven - ``` - -其他安装方法,请参考 [Maven 官方文档](https://maven.apache.org/install.html)。 - -## 第 4 步:获取应用程序代码 - -> **建议:** +> **注意** > -> 如果你希望得到一个与本示例相同依赖的空白程序,而无需示例代码,可参考[创建相同依赖空白程序(可选)](#创建相同依赖空白程序可选)一节。 - -请下载或克隆示例代码库 [pingcap-inc/tidb-example-java](https://github.com/pingcap-inc/tidb-example-java),并进入到目录 `spring-jpa-hibernate` 中。 - -## 第 5 步:运行应用程序 - -接下来运行应用程序代码,将会生成一个 Web 应用程序。Hibernate 将在数据库 `test` 中创建一个表 `player_jpa`。如果你向应用程序的 Restful API 发送请求,这些请求将会在 TiDB 集群上运行[数据库事务](/develop/dev-guide-transaction-overview.md)。 - -如果你想了解有关此应用程序的代码的详细信息,可参阅[实现细节](#实现细节)部分。 - -### 第 5 步第 1 部分:TiDB Cloud 更改参数 - -若你使用 TiDB Serverless 集群,更改 `application.yml`(位于 `src/main/resources` 内)关于 `spring.datasource.url`、`spring.datasource.username`、`spring.datasource.password` 的参数: - -```yaml -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 Serverless 集群面板中得到的连接信息为: - -- Endpoint: `xxx.tidbcloud.com` -- Port: `4000` -- User: `2aEp24QWEDLqRFs.root` - -那么此处应将参数更改为: - -```yaml -spring: - datasource: - url: jdbc:mysql://xxx.tidbcloud.com:4000/test?sslMode=VERIFY_IDENTITY&enabledTLSProtocols=TLSv1.2,TLSv1.3 - username: 2aEp24QWEDLqRFs.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 部分:运行 +> 本文档适用于 TiDB Serverless、TiDB Dedicated 和本地部署的 TiDB。 -打开终端,进入 `tidb-example-java/spring-jpa-hibernate` 代码示例目录: +## 前置需求 -```shell -cd /tidb-example-java/spring-jpa-hibernate -``` - -#### 使用 Make 构建并运行(推荐) - -```shell -make -``` - -#### 手动构建并运行 - -推荐你使用 Make 方式进行构建并运行,当然,若你希望手动进行构建,请依照以下步骤逐步运行,可以得到相同的结果: +- 推荐 **Java Development Kit** (JDK) **17** 及以上版本。你可以根据公司及个人需求,自行选择 [OpenJDK](https://openjdk.org/) 或 [Oracle JDK](https://www.oracle.com/hk/java/technologies/downloads/)。 +- [Maven](https://maven.apache.org/install.html) **3.8** 及以上版本。 +- [Git](https://git-scm.com/downloads)。 +- TiDB 集群。如果你还没有 TiDB 集群,可以按照以下方式创建: + - (推荐方式)参考[创建 TiDB Serverless 集群](/develop/dev-guide-build-cluster-in-cloud.md#第-1-步创建-tidb-serverless-集群),创建你自己的 TiDB Cloud 集群。 + - 参考[部署本地测试 TiDB 集群](/quick-start-with-tidb.md#部署本地测试集群)或[部署正式 TiDB 集群](/production-deployment-using-tiup.md),创建本地集群。 -清除缓存并打包: +## 运行代码并连接到 TiDB -```shell -mvn clean package -``` - -运行应用程序的 JAR 文件: - -```shell -java -jar target/spring-jpa-hibernate-0.0.1.jar -``` +本小节演示如何运行示例应用程序的代码,并连接到 TiDB。 -### 第 5 步第 3 部分:输出 +### 第 1 步:克隆示例代码仓库到本地 -输出的最后部分应如下所示: +运行以下命令,将示例代码仓库克隆到本地: +```bash +git clone https://github.com/tidb-samples/tidb-java-springboot-jpa-quickstart.git +cd tidb-java-springboot-jpa-quickstart ``` - . ____ _ __ _ _ - /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ -( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ - \\/ ___)| |_)| | | | | || (_| | ) ) ) ) - ' |____| .__|_| |_|_| |_\__, | / / / / - =========|_|==============|___/=/_/_/_/ - :: Spring Boot :: (v3.0.1) - -2023-01-05T14:06:54.427+08:00 INFO 22005 --- [ main] com.pingcap.App : Starting App using Java 17.0.2 with PID 22005 (/Users/cheese/IdeaProjects/tidb-example-java/spring-jpa-hibernate/target/classes started by cheese in /Users/cheese/IdeaProjects/tidb-example-java) -2023-01-05T14:06:54.428+08:00 INFO 22005 --- [ main] com.pingcap.App : No active profile set, falling back to 1 default profile: "default" -2023-01-05T14:06:54.642+08:00 INFO 22005 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. -2023-01-05T14:06:54.662+08:00 INFO 22005 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 17 ms. Found 1 JPA repository interfaces. -2023-01-05T14:06:54.830+08:00 INFO 22005 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http) -2023-01-05T14:06:54.833+08:00 INFO 22005 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat] -2023-01-05T14:06:54.833+08:00 INFO 22005 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.4] -2023-01-05T14:06:54.865+08:00 INFO 22005 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext -2023-01-05T14:06:54.865+08:00 INFO 22005 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 421 ms -2023-01-05T14:06:54.916+08:00 INFO 22005 --- [ main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] -2023-01-05T14:06:54.929+08:00 INFO 22005 --- [ main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.1.6.Final -2023-01-05T14:06:54.969+08:00 WARN 22005 --- [ main] org.hibernate.orm.deprecation : HHH90000021: Encountered deprecated setting [javax.persistence.sharedCache.mode], use [jakarta.persistence.sharedCache.mode] instead -2023-01-05T14:06:55.005+08:00 INFO 22005 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... -2023-01-05T14:06:55.074+08:00 INFO 22005 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@5e905f2c -2023-01-05T14:06:55.075+08:00 INFO 22005 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. -2023-01-05T14:06:55.089+08:00 INFO 22005 --- [ 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 -2023-01-05T14:06:55.332+08:00 INFO 22005 --- [ main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform] -2023-01-05T14:06:55.335+08:00 INFO 22005 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' -2023-01-05T14:06:55.579+08:00 WARN 22005 --- [ 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 -2023-01-05T14:06:55.710+08:00 INFO 22005 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' -2023-01-05T14:06:55.714+08:00 INFO 22005 --- [ main] com.pingcap.App : Started App in 1.432 seconds (process running for 1.654) -``` - -输出日志中,提示应用程序在启动过程中做了什么,这里显示应用程序使用 [Tomcat](https://tomcat.apache.org/) 启动了一个 **Servlet**,使用 Hibernate 作为 ORM,[HikariCP](https://github.com/brettwooldridge/HikariCP) 作为数据库连接池的实现,使用了 `org.hibernate.dialect.TiDBDialect` 作为数据库方言。启动后,Hibernate 删除并重新创建了表 `player_jpa`,及序列 `player_jpa_id_seq`。在启动的最后,监听了 8080 端口,对外提供 HTTP 服务。 -如果你想了解有关此应用程序的代码的详细信息,可参阅本教程下方的[实现细节](#实现细节)。 +### 第 2 步:配置连接信息 -## 第 6 步:HTTP 请求 +根据不同的 TiDB 部署方式,使用不同的方法连接到 TiDB 集群。 -在运行应用程序后,你可以通过访问根地址 `http://localhost:8000` 向后端程序发送 HTTP 请求。下面将给出一些示例请求来演示如何使用该服务。 + - +
-
+1. 在 TiDB Cloud 的 [**Clusters**](https://tidbcloud.com/console/clusters) 页面中,选择你的 TiDB Serverless 集群,进入集群的 **Overview** 页面。 -1. 将配置文件 [`Player.postman_collection.json`](https://raw.githubusercontent.com/pingcap-inc/tidb-example-python/main/django_example/Player.postman_collection.json) 导入 [Postman](https://www.postman.com/)。 +2. 点击右上角的 **Connect** 按钮,将会弹出连接对话框。 -2. 导入后 **Collections** > **Player** 如图所示: +3. 确认对话框中的配置和你的运行环境一致。 - ![postman import](/media/develop/postman_player_import.png) + - **Endpoint Type** 为 `Public`。 + - **Connect With** 选择 `General`。 + - **Operating System** 为你的运行环境。 -3. 发送请求: + > **Tip:** + > + > 如果你在 Windows Subsystem for Linux (WSL) 中运行,请切换为对应的 Linux 发行版。 - - 增加玩家 +4. 如果你还没有设置密码,点击 **Create password** 生成一个随机密码。 - 点击 **Create** 标签,点击 **Send** 按钮,发送 `POST` 形式的 `http://localhost:8000/player/` 请求。返回值为增加的玩家个数,预期为 1。 + > **Tip:** + > + > 如果你之前已经生成过密码,可以直接使用原密码,或点击 **Reset password** 重新生成密码。 - ![Postman-Create](/media/develop/postman_player_create.png) +5. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - - 使用 ID 获取玩家信息 - - 点击 **GetByID** 标签,点击 **Send** 按钮,发送 `GET` 形式的 `http://localhost:8000/player/1` 请求。返回值为 ID 为 1 的玩家信息。 - - ![Postman-GetByID](/media/develop/postman_player_getbyid.png) - - - 使用 Limit 批量获取玩家信息 - - 点击 **GetByLimit** 标签,点击 **Send** 按钮,发送 `GET` 形式的 `http://localhost:8000/player/limit/3` 请求。返回值为最多 3 个玩家的信息列表。 - - ![Postman-GetByLimit](/media/develop/postman_player_getbylimit.png) - - - 分页获取玩家信息 - - 点击 **GetByPage** 标签,点击 **Send** 按钮,发送 `GET` 形式的 `http://localhost:8080/player/page?index=0&size=2` 请求。返回值为 index 为 0 的页,每页有 2 个玩家信息列表。此外,还包含了分页信息,如偏移量、总页数、是否排序等。 - - ![Postman-GetByPage](/media/develop//postman_player_getbypage.png) - - - 获取玩家个数 + ```bash + cp env.sh.example env.sh + ``` - 点击 **Count** 标签,点击 **Send** 按钮,发送 `GET` 形式的 `http://localhost:8000/player/count` 请求。返回值为玩家个数。 +6. 复制并粘贴对应连接字符串至 `env.sh` 中。需更改部分示例结果如下: - ![Postman-Count](/media/develop/postman_player_count.png) + ```shell + export TIDB_HOST='{host}' # e.g. gateway01.ap-northeast-1.prod.aws.tidbcloud.com + export TIDB_PORT='4000' + export TIDB_USER='{user}' # e.g. xxxxxx.root + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='true' + ``` - - 玩家交易 + 注意替换 `{}` 中的占位符为连接对话框中获得的值。 - 点击 **Trade** 标签,点击 **Send** 按钮,发送 `PUT` 形式的 `http://localhost:8000/player/trade` 请求。请求参数为售卖玩家 ID `sellID`、购买玩家 ID `buyID`、购买货物数量 `amount` 以及购买消耗金币数 `price`。返回值为交易是否成功。当出现售卖玩家货物不足、购买玩家金币不足或数据库错误时,交易将不成功。并且由于[数据库事务](/develop/dev-guide-transaction-overview.md)保证,不会有玩家的金币或货物丢失的情况。 + TiDB Serverless 要求使用 TLS (SSL) connection,因此 `USE_SSL` 的值应为 `true`。 - ![Postman-Trade](/media/develop/postman_player_trade.png) +7. 保存 `env.sh` 文件。
-
+
-下面使用 curl 请求服务端。 +1. 在 TiDB Cloud 的 [**Clusters**](https://tidbcloud.com/console/clusters) 页面中,选择你的 TiDB Dedicated 集群,进入集群的 **Overview** 页面。 -- 增加玩家 +2. 点击右上角的 **Connect** 按钮,将会出现连接对话框。 - 使用 `POST` 方法向 `/player` 端点发送请求来增加玩家,例如: +3. 在对话框中点击 **Allow Access from Anywhere**。 - ```shell - curl --location --request POST 'http://localhost:8080/player/' --header 'Content-Type: application/json' --data-raw '[{"coins":100,"goods":20}]' - ``` + 更多配置细节,可参考 [TiDB Dedicated 标准连接教程(英文)](https://docs.pingcap.com/tidbcloud/connect-via-standard-connection)。 - 这里使用 JSON 作为信息的载荷。表示需要创建一个金币数 `coins` 为 100,货物数 `goods` 为 20 的玩家。返回值为创建的玩家信息: +4. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - ```json - 1 + ```bash + cp env.sh.example env.sh ``` -- 使用 ID 获取玩家信息 - - 使用 `GET` 方法向 `/player` 端点发送请求来获取玩家信息。此外,还需要在路径上给出玩家的 ID 参数,即 `/player/{id}`。例如,在请求 ID 为 1 的玩家时: +5. 复制并粘贴对应的连接字符串至 `env.sh` 中。需更改部分示例结果如下: ```shell - curl --location --request GET 'http://localhost:8080/player/1' + export TIDB_HOST='{host}' # e.g. tidb.xxxx.clusters.tidb-cloud.com + export TIDB_PORT='4000' + export TIDB_USER='{user}' # e.g. root + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='false' ``` - 返回值为 ID 为 1 的玩家的信息: + 注意替换 `{}` 中的占位符为连接对话框中获得的值。 - ```json - { - "coins": 200, - "goods": 10, - "id": 1 - } - ``` +6. 保存 `env.sh` 文件。 -- 使用 Limit 批量获取玩家信息 +
- 使用 `GET` 方法向 `/player/limit` 端点发送请求来获取玩家信息。此外,还需要在路径上给出限制查询的玩家信息的总数,即 `/player/limit/{limit}`。例如,在请求最多 3 个玩家的信息时: +
- ```shell - curl --location --request GET 'http://localhost:8080/player/limit/3' - ``` +1. 运行以下命令,将 `env.sh.example` 复制并重命名为 `env.sh`: - 返回值为玩家信息的列表: - - ```json - [ - { - "coins": 200, - "goods": 10, - "id": 1 - }, - { - "coins": 0, - "goods": 30, - "id": 2 - }, - { - "coins": 100, - "goods": 20, - "id": 3 - } - ] + ```bash + cp env.sh.example env.sh ``` -- 分页获取玩家信息 - - 使用 `GET` 方法向 `/player/page` 端点发送请求来分页获取玩家信息。额外地需要使用 URL 参数,例如在请求页面序号 `index` 为 0,每页最大请求量 `size` 为 2 时: +2. 复制并粘贴对应 TiDB 的连接字符串至 `env.sh` 中。需更改部分示例结果如下: ```shell - curl --location --request GET 'http://localhost:8080/player/page?index=0&size=2' + export TIDB_HOST='{host}' + export TIDB_PORT='4000' + export TIDB_USER='root' + export TIDB_PASSWORD='{password}' + export TIDB_DB_NAME='test' + export USE_SSL='false' ``` - 返回值为 `index` 为 0 的页,每页有 2 个玩家信息列表。此外,还包含了分页信息,如偏移量、总页数、是否排序等。 - - ```json - { - "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 - } - ``` - -- 获取玩家个数 + 注意替换 `{}` 中的占位符为你的 TiDB 对应的值,并设置 `USE_SSL` 为 `false`。如果你在本机运行 TiDB,默认 Host 地址为 `127.0.0.1`,密码为空。 - 使用 `GET` 方法向 `/player/count` 端点发送请求来获取玩家个数: +3. 保存 `env.sh` 文件。 - ```shell - curl --location --request GET 'http://localhost:8080/player/count' - ``` - - 返回值为玩家个数: +
- ```json - 4 - ``` + -- 玩家交易 +### 第 3 步:运行代码并查看结果 - 使用 `PUT` 方法向 `/player/trade` 端点发送请求来发起玩家间的交易,例如: +1. 运行下述命令,启动示例代码编写的服务: ```shell - 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' + make ``` - 这里使用 Form Data 作为信息的载荷。表示售卖玩家 ID `sellID` 为 1、购买玩家 ID `buyID` 为 2、购买货物数量 `amount` 为 10、购买消耗金币数 `price` 为 100。 - - 返回值为交易是否成功: +2. 打开另一个终端,开启请求脚本: + ```shell + make request ``` - true - ``` - - 当出现售卖玩家货物不足、购买玩家金币不足或数据库错误时,交易将不成功。并且由于[数据库事务](/develop/dev-guide-transaction-overview.md)保证,不会有玩家的金币或货物丢失的情况。 - -
-
+3. 查看 [`Expected-Output.txt`](https://github.com/tidb-samples/tidb-java-springboot-jpa-quickstart/blob/main/Expected-Output.txt),并与你的服务程序输出进行比较。结果近似即为连接成功。 -为方便测试,你可以使用 [`request.sh`](https://github.com/pingcap-inc/tidb-example-java/blob/main/spring-jpa-hibernate/request.sh) 脚本依次发送以下请求: +## 示例代码片段 -1. 循环创建 10 名玩家 -2. 获取 ID 为 1 的玩家信息 -3. 获取至多 3 名玩家信息列表 -4. 获取 `index` 为 0,`size` 为 2 的一页玩家信息 -5. 获取玩家总数 -6. ID 为 1 的玩家作为售出方,ID 为 2 的玩家作为购买方,购买 10 个货物,耗费 100 金币 +你可参考以下关键代码片段,完成自己的应用开发。 -使用 `make request` 或 `./request.sh` 命令运行此脚本,运行结果如下所示: +完整代码及其运行方式,见代码仓库 [tidb-java-springboot-jpa-quickstart](https://github.com/tidb-samples/tidb-java-springboot-jpa-quickstart/blob/main/README-zh.md)。 -```shell -> make request -./request.sh -loop to create 10 players: -1111111111 +### 连接到 TiDB -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` 是项目实现与数据库连接并完成数据持久化的包 - -### 配置 - -本节将简要介绍 `pom.xml` 文件中的 Maven 配置,及 `application.yml` 文件中的用户配置。 - -#### Maven 配置 - -`pom.xml` 文件为 Maven 配置,在文件内声明了项目的 Maven 依赖,打包方法,打包信息等,你可以通过[创建相同依赖空白程序](#创建相同依赖空白程序可选) 这一节来复刻此配置文件的生成流程,当然,也可直接复制至你的项目来使用。 - -```xml - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.0.1 - - - - com.pingcap - spring-jpa-hibernate - 0.0.1 - spring-jpa-hibernate - an example for spring boot, jpa, hibernate and TiDB - - - 17 - 17 - 17 - - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - - org.springframework.boot - spring-boot-starter-web - - - - mysql - mysql-connector-java - runtime - - - - org.springframework.boot - spring-boot-starter-test - test - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - -``` - -#### 用户配置 - -`application.yml` 此配置文件声明了用户配置,如数据库地址、密码、使用的数据库方言等。 +编写配置文件 `application.yml`: ```yaml spring: datasource: - url: jdbc:mysql://localhost:4000/test - username: root - # password: xxx + url: ${TIDB_JDBC_URL:jdbc:mysql://localhost:4000/test} + username: ${TIDB_USER:root} + password: ${TIDB_PASSWORD:} driver-class-name: com.mysql.cj.jdbc.Driver jpa: show-sql: true @@ -578,442 +193,65 @@ spring: ddl-auto: create-drop ``` -此配置格式为 [YAML](https://yaml.org/) 格式。其中: +请在配置后将环境变量 `TIDB_JDBC_URL`、`TIDB_USER` 和 `TIDB_PASSWORD` 设置为你的 TiDB 集群的实际值。此配置文件带有环境变量默认配置,即在不配置环境变量时,变量的值为: -- `spring.datasource.url`:数据库连接的 URL。 -- `spring.datasource.url`:数据库用户名。 -- `spring.datasource.password`:数据库密码,此项为空,需注释或删除。 -- `spring.datasource.driver-class-name`:数据库驱动,因为 TiDB 与 MySQL 兼容,则此处使用与 mysql-connector-java 适配的驱动类 `com.mysql.cj.jdbc.Driver`。 -- `jpa.show-sql`:为 true 时将打印 JPA 运行的 SQL。 -- `jpa.database-platform`:选用的数据库方言,此处连接了 TiDB,自然选择 TiDB 方言,注意,此方言在 6.0.0.Beta2 版本后的 Hibernate 中才可选择,请注意依赖版本。 -- `jpa.hibernate.ddl-auto`:此处选择的 create-drop 将会在程序开始时创建表,退出时删除表。请勿在正式环境使用,但此处为示例程序,希望尽量不影响数据库数据,因此选择了此选项。 +- `TIDB_JDBC_URL`: `"jdbc:mysql://localhost:4000/test"` +- `TIDB_USER`: `"root"` +- `TIDB_PASSWORD`: `""` -### 入口文件 +### 数据管理:`@Repository` -入口文件 `App.java`: +Spring Data JPA 通过 `@Repository` 接口来管理数据。你需要继承 `JpaRepository` 接口,以使用其提供的增删改查函数。 ```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](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/SpringBootApplication.html)。有关详细信息,请参阅 Spring Boot 官方文档中的 [Using the @SpringBootApplication Annotation](https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-using-springbootapplication-annotation)。随后,使用 `ApplicationPidFileWriter` 在程序启动过程中,写下一个名为 `spring-jpa-hibernate.pid` 的 PID (process identification number) 文件,可从外部使用此 PID 文件关闭此应用程序。 - -### 数据库持久层 - -数据库持久层,即 `dao` 包内,实现了数据对象的持久化。 - -#### 实体对象 - -`PlayerBean.java` 文件为实体对象,这个对象对应了数据库的一张表。 - -```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` 声明一个使用[序列](/sql-statements/sql-statement-create-sequence.md)的生成器,使用注解属性 `name` 声明生成器的名称为 `player_id` (与 `@GeneratedValue` 中指定的名称需保持一致)。随后使用注解属性 `sequenceName` 指定数据库中序列的名称。最后,使用注解属性 `allocationSize` 声明序列的步长为 1。 -- `@Column` 将每个私有属性声明为表 `player_jpa` 的一列,使用注解属性 `name` 确定属性对应的列名。 - -#### 存储库 - -为了抽象数据库层,Spring 应用程序使用 [Repository](https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories) 接口,或者 Repository 的子接口。 这个接口映射到一个数据库对象,常见的,比如会映射到一个表上。JPA 会实现一些预制的方法,比如 [INSERT](/sql-statements/sql-statement-insert.md),或使用主键的 [SELECT](/sql-statements/sql-statement-select.md) 等。 - -```java -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 { - /** - * 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 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 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 此接口如何实现查询。在此处使用了两种查询语句的语法,其中,在接口 `getPlayersByPage` 中的查询语句使用的是一种被 Hibernate 称为 [HQL](https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#hql) (Hibernate Query Language) 的语法。而接口 `getPlayersByLimit` 中使用的是普通的 SQL,在使用 SQL 语法时,需要将 `@Query` 的注解参数 `nativeQuery` 设置为 true。 - -在 `getPlayersByLimit` 注解的 SQL 中,`:limit` 在 Hibernate 中被称为[命名参数](https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#jpql-query-parameters),Hibernate 将按名称自动寻找并拼接注解所在接口内的参数。你也可以使用 `@Param` 来指定与参数不同的名称用于注入。 - -在 `getPlayerAndLock` 中,使用了一个注解 [@Lock](https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/Lock.html),此注解声明此处使用悲观锁进行锁定,如需了解更多其他锁定方式,可查看[实体锁定](https://openjpa.apache.org/builds/2.2.2/apache-openjpa/docs/jpa_overview_em_locking.html)文档。此处的 `@Lock` 仅可与 HQL 搭配使用,否则将会产生错误。当然,如果你希望直接使用 SQL 进行锁定,可直接使用注释部分的注解: +随后,在需要使用 `PlayerRepository` 的类中,你可以通过 `@Autowired` 自动装配,这样就可以直接使用增删改查函数了。示例代码如下: ```java -@Query(value = "SELECT * FROM player_jpa WHERE id = :id FOR UPDATE", nativeQuery = true) +@Autowired +private PlayerRepository playerRepository; ``` -直接使用 SQL 的 `FOR UPDATE` 来增加锁。你也可通过 TiDB [SELECT 文档](/sql-statements/sql-statement-select.md) 进行更深层次的原理学习。 - -### 逻辑实现 - -逻辑实现层,即 `service` 包,内含了项目实现的接口与逻辑 - -#### 接口 - -`PlayerService.java` 文件内定义了逻辑接口,实现接口,而不是直接编写一个类的原因,是尽量使例子贴近实际使用,体现设计的开闭原则。你也可以省略掉此接口,在依赖类中直接注入实现类,但并不推荐这样做。 +### 插入或更新数据 ```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 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 getPlayers(Integer limit); - - /** - * get a page of players from the data store. - * - * @param index page index - * @param size page size - * @return player list - */ - Page getPlayersByPage(Integer index, Integer size); - - /** - * count players from the data store. - * - * @return all players count - */ - Long countPlayers(); -} +playerRepository.save(player); ``` -#### 实现(重要) +更多信息参考[插入数据](/develop/dev-guide-insert-data.md)和[更新数据](/develop/dev-guide-update-data.md)。 -`PlayerService.java` 文件内实现了 `PlayerService` 接口,所有数据操作逻辑都编写在这里。 +### 查询数据 ```java -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 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 getPlayers(Integer limit) { - return playerRepository.getPlayersByLimit(limit); - } - - @Override - public Page getPlayersByPage(Integer index, Integer size) { - return playerRepository.getPlayersByPage(PageRequest.of(index, size)); - } - - @Override - public Long countPlayers() { - return playerRepository.count(); - } -} +PlayerBean player = playerRepository.findById(id).orElse(null); ``` -这里使用了 `@Service` 这个注解,声明此对象的生命周期交由 Spring 管理。 - -注意,除了有 `@Service` 注解之外,PlayerServiceImpl 实现类还有一个 [@Transactional](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction-declarative-annotations) 注解。当在应用程序中启用事务管理时 (可使用 [@EnableTransactionManagement](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/transaction/annotation/EnableTransactionManagement.html) 打开,但 Spring Boot 默认开启,无需再次手动配置),Spring 会自动将所有带有 `@Transactional` 注释的对象包装在一个代理中,使用该代理对对象的调用进行处理。 - -你可以简单的认为,代理在带有 `@Transactional` 注释的对象内的函数调用时:在函数顶部将使用 `transaction.begin()` 开启事务,函数返回后,调用 `transaction.commit()` 进行事务提交,而出现任何运行时错误时,代理将会调用 `transaction.rollback()` 来回滚。 - -你可参阅[数据库事务](/develop/dev-guide-transaction-overview.md)来获取更多有关事务的信息,或者阅读 Spring 官网中的文章[理解 Spring 框架的声明式事务实现](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-decl-explained)。 - -整个实现类中,`buyGoods` 函数需重点关注,其在不符合逻辑时将抛出异常,引导 Hibernate 进行事务回滚,防止出现错误数据。 +更多信息参考[查询数据](/develop/dev-guide-get-data-from-single-table.md)。 -### 外部接口 - -`controller` 包对外暴露 HTTP 接口,可以通过 [REST API](https://www.redhat.com/en/topics/api/what-is-a-rest-api#) 来访问服务。 +### 删除数据 ```java -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 playerList) { - return playerService.createPlayers(playerList); - } - - @GetMapping("/{id}") - public PlayerBean getPlayerByID(@PathVariable Long id) { - return playerService.getPlayerByID(id); - } - - @GetMapping("/limit/{limit_size}") - public List getPlayerByLimit(@PathVariable("limit_size") Integer limit) { - return playerService.getPlayers(limit); - } - - @GetMapping("/page") - public Page 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; - } -} +playerRepository.deleteById(id); ``` -`PlayerController` 中使用了尽可能多的注解方式来作为示例展示功能,在实际项目中,请尽量保持风格的统一,同时遵循你公司或团体的规则。`PlayerController` 有许多注解,下方将进行逐一解释: - -- [@RestController](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RestController.html) 将 `PlayerController` 声明为一个 [Web Controller](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller),且将返回值序列化为 JSON 输出。 -- [@RequestMapping](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestMapping.html) 映射 URL 端点为 `/player`,即此 `Web Controller` 仅监听 `/player` URL 下的请求。 -- `@Autowired` 用于 Spring 的自动装配,可以看到,此处声明需要一个 `PlayerService` 对象,此对象为接口,并未指定使用哪一个实现类,这是由 Spring 自动装配的,有关此装配规则,可查看 Spirng 官网中的 [The IoC container](https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/beans.html) 一文。 -- [@PostMapping](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/PostMapping.html) 声明此函数将响应 HTTP 中的 [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST) 类型请求。 - - `@RequestBody` 声明此处将 HTTP 的整个载荷解析到参数 `playerList` 中。 - - `@NonNull` 声明参数不可为空,否则将校验并返回错误。 -- [@GetMapping](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/GetMapping.html) 声明此函数将响应 HTTP 中的 [GET](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) 类型请求。 - - [@PathVariable](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/PathVariable.html) 可以看到注解中有形如 `{id}` 、`{limit_size}` 这样的占位符,这种占位符将被绑定到 `@PathVariable` 注释的变量中,绑定的依据是注解中的注解属性 `name`(变量名可省略,即 `@PathVariable(name="limit_size")` 可写成 `@PathVariable("limit_size")` ),不特殊指定时,与变量名名称相同。 -- [@PutMapping](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/PutMapping.html) 声明此函数将响应 HTTP 中的 [PUT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) 类型请求。 -- [@RequestParam](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/RequestParam.html) 此声明将解析请求中的 URL 参数、表单参数等参数,绑定至注解的变量中。 - -## 创建相同依赖空白程序(可选) - -本程序使用 [Spring Initializr](https://start.spring.io/) 构建。你可以在这个网页上通过点选以下选项并更改少量配置,来快速得到一个与本示例程序相同依赖的空白应用程序,配置项如下: +更多信息参考[删除数据](/develop/dev-guide-delete-data.md)。 -**Project** +## 下一步 -- Maven Project +- 关于本文使用到的第三方库及框架,可以参考各自官方文档: -**Language** + - [Spring Framework 官方文档](https://spring.io/projects/spring-framework) + - [Spring Boot 官方文档](https://spring.io/projects/spring-boot) + - [Spring Data JPA 官方文档](https://spring.io/projects/spring-data-jpa) + - [Hibernate 官方文档](https://hibernate.org/orm/documentation) +- 你可以继续阅读开发者文档,以获取更多关于 TiDB 应用开发的最佳实践。例如:[插入数据](/develop/dev-guide-insert-data.md)、[更新数据](/develop/dev-guide-update-data.md)、[删除数据](/develop/dev-guide-delete-data.md)、[单表读取](/develop/dev-guide-get-data-from-single-table.md)、[事务](/develop/dev-guide-transaction-overview.md)、[SQL 性能优化](/develop/dev-guide-optimize-sql-overview.md)等。 +- 如果你更倾向于参与课程进行学习,我们也提供专业的 [TiDB 开发者课程](https://cn.pingcap.com/courses-catalog/back-end-developer/?utm_source=docs-cn-dev-guide)支持,并在考试后提供相应的[资格认证](https://learn.pingcap.com/learner/certification-center)。 +- 我们还额外提供针对 Java 开发者的课程:[使用 Connector/J - TiDB v6](https://learn.pingcap.com/learner/course/840002/?utm_source=docs-cn-dev-guide) 及[在 TiDB 上开发应用的最佳实践 - TiDB v6](https://learn.pingcap.com/learner/course/780002/?utm_source=docs-cn-dev-guide)。 -- Java - -**Spring Boot** - -- 最新稳定版本 - -**Project Metadata** - -- Group: com.pingcap -- Artifact: spring-jpa-hibernate -- Name: spring-jpa-hibernate -- Package name: com.pingcap -- Packaging: Jar -- Java: 17 - -**Dependencies** - -- Spring Web -- Spring Data JPA -- MySQL Driver - -> **注意:** -> -> 尽管 SQL 相对标准化,但每个数据库供应商都使用 ANSI SQL 定义语法的子集和超集。这被称为数据库的方言。 Hibernate 通过其 org.hibernate.dialect.Dialect 类和每个数据库供应商的各种子类来处理这些方言的变化。 -> -> 在大多数情况下,Hibernate 将能够通过在启动期间通过 JDBC 连接的一些返回值来确定要使用的正确方言。有关 Hibernate 确定要使用的正确方言的能力(以及你影响该解析的能力)的信息,请参阅[方言解析](https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#portability-dialectresolver)。 -> -> 如果由于某种原因无法确定正确的方言,或者你想使用自定义方言,则需要设置 hibernate.dialect 配置项。 -> -> _—— 节选自 Hibernate 官方文档: [Database Dialect](https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#database-dialect)_ +## 需要帮助? -随后,即可获取一个拥有与示例程序相同依赖的空白 **Spring Boot** 应用程序。 \ No newline at end of file +如果在开发的过程中遇到问题,可以在 [AskTUG](https://asktug.com/?utm_source=docs-cn-dev-guide) 上进行提问,寻求帮助。