From e0bb88c90225f818b467137257c8d5733aa9780d Mon Sep 17 00:00:00 2001 From: Ti Chi Robot Date: Thu, 23 Mar 2023 21:36:42 +0800 Subject: [PATCH] develop: Develop Guide Python Examples - English Edition (#11742) (#12997) --- TOC-tidb-cloud.md | 5 +- TOC.md | 7 +- develop/dev-guide-choose-driver-or-orm.md | 63 +- develop/dev-guide-delete-data.md | 59 + develop/dev-guide-insert-data.md | 44 +- ...-optimistic-and-pessimistic-transaction.md | 225 ++++ ...ide-outdated-for-python-mysql-connector.md | 188 --- develop/dev-guide-outdated-for-sqlalchemy.md | 218 ---- .../dev-guide-sample-application-python.md | 1103 +++++++++++++++++ develop/dev-guide-third-party-support.md | 315 ++++- 10 files changed, 1769 insertions(+), 458 deletions(-) delete mode 100644 develop/dev-guide-outdated-for-python-mysql-connector.md delete mode 100644 develop/dev-guide-outdated-for-sqlalchemy.md create mode 100644 develop/dev-guide-sample-application-python.md diff --git a/TOC-tidb-cloud.md b/TOC-tidb-cloud.md index 522afa658bc4..0f996c12ce8c 100644 --- a/TOC-tidb-cloud.md +++ b/TOC-tidb-cloud.md @@ -18,9 +18,10 @@ - [Build a TiDB Cluster in TiDB Cloud (Serverless Tier)](/develop/dev-guide-build-cluster-in-cloud.md) - [CRUD SQL in TiDB](/develop/dev-guide-tidb-crud-sql.md) - Example Applications - - [Java](/develop/dev-guide-sample-application-java.md) - - [Java (Spring Boot)](/develop/dev-guide-sample-application-spring-boot.md) - [Golang](/develop/dev-guide-sample-application-golang.md) + - [Java (Spring Boot)](/develop/dev-guide-sample-application-spring-boot.md) + - [Java](/develop/dev-guide-sample-application-java.md) + - [Python](/develop/dev-guide-sample-application-python.md) - Connect to TiDB - [Choose Driver or ORM](/develop/dev-guide-choose-driver-or-orm.md) - [Connection Pools and Connection Parameters](/develop/dev-guide-connection-parameters.md) diff --git a/TOC.md b/TOC.md index 954bab93fb51..835e7fc92f11 100644 --- a/TOC.md +++ b/TOC.md @@ -21,9 +21,10 @@ - [Build a TiDB Cluster in TiDB Cloud (Serverless Tier)](/develop/dev-guide-build-cluster-in-cloud.md) - [CRUD SQL in TiDB](/develop/dev-guide-tidb-crud-sql.md) - Example Applications - - [Java](/develop/dev-guide-sample-application-java.md) - - [Java (Spring Boot)](/develop/dev-guide-sample-application-spring-boot.md) - [Golang](/develop/dev-guide-sample-application-golang.md) + - [Java (Spring Boot)](/develop/dev-guide-sample-application-spring-boot.md) + - [Java](/develop/dev-guide-sample-application-java.md) + - [Python](/develop/dev-guide-sample-application-python.md) - Connect to TiDB - [Choose Driver or ORM](/develop/dev-guide-choose-driver-or-orm.md) - [Connect to TiDB](/develop/dev-guide-connect-to-tidb.md) @@ -76,8 +77,6 @@ - [SQL Development Specifications](/develop/dev-guide-sql-development-specification.md) - Legacy Docs - [For Django](/develop/dev-guide-outdated-for-django.md) - - [For SQLAlchemy](/develop/dev-guide-outdated-for-sqlalchemy.md) - - [For mysql-connector-python](/develop/dev-guide-outdated-for-python-mysql-connector.md) - Cloud Native Development Environment - [Gitpod](/develop/dev-guide-playground-gitpod.md) - Third-Party Support diff --git a/develop/dev-guide-choose-driver-or-orm.md b/develop/dev-guide-choose-driver-or-orm.md index 7a1e86b1f142..81da8df430d1 100644 --- a/develop/dev-guide-choose-driver-or-orm.md +++ b/develop/dev-guide-choose-driver-or-orm.md @@ -9,8 +9,8 @@ summary: Learn how to choose a driver or ORM framework to connect to TiDB. > > TiDB provides the following two support levels for drivers and ORMs: > -> - **Full**: indicates that using this driver or ORM does not have any known issues. -> - **Verified**: indicates that using this driver or ORM might get errors because of compatibility differences between TiDB and MySQL. +> - **Full**: indicates that TiDB is compatible with most features of the tool and maintains compatibility with its newer versions. PingCAP will periodically conduct compatibility tests with the latest version of [Third-party tools supported by TiDB](/develop/dev-guide-third-party-support.md). +> - **Compatible**: indicates that because the corresponding third-party tool is adapted to MySQL and TiDB is highly compatible with the MySQL protocol, so TiDB can use most features of the tool. However, PingCAP has not completed a full test on all features of the tool, which might lead to some unexpected behaviors. > > For more information, refer to [Third-Party Tools Supported by TiDB](/develop/dev-guide-third-party-support.md). @@ -234,6 +234,65 @@ go get -u gorm.io/driver/mysql For an example of using GORM to build a TiDB application, see [Build a Simple CRUD App with TiDB and Golang](/develop/dev-guide-sample-application-golang.md). +## Python + +This section describes how to use drivers and ORM frameworks in Python. + +### Python drivers + + +
+ +Support level: **Compatible** + +You can follow the [PyMySQL documentation](https://pypi.org/project/PyMySQL/) to download and configure the driver. It is recommended to use PyMySQL 1.0.2 or later versions. + +For an example of using PyMySQL to build a TiDB application, see [Build a Simple CRUD App with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code). + +
+
+ +Support level: **Compatible** + +You can follow the [mysqlclient documentation](https://pypi.org/project/mysqlclient/) to download and configure the driver. It is recommended to use mysqlclient 2.1.1 or later versions. + +For an example of using mysqlclient to build a TiDB application, see [Build a Simple CRUD App with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code). + +
+
+ +Support level: **Compatible** + +You can follow the [mysql-connector-python documentation](https://dev.mysql.com/doc/connector-python/en/connector-python-installation-binary.html) to download and configure the driver. It is recommended to use Connector/Python 8.0.31 or later versions. + +For an example of using mysql-connector-python to build a TiDB application, see [Build a Simple CRUD App with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code). + +
+
+ +### Python ORM frameworks + + +
+ +Support level: **Compatible** + +[SQLAlchemy](https://www.sqlalchemy.org/) is a popular ORM framework for Python. To get all dependencies in your application, you can use the `pip install SQLAlchemy==1.4.44` command. It is recommended to use SQLAlchemy 1.4.44 or later versions. + +For an example of using SQLAlchemy to build a TiDB application, see [Build a Simple CRUD App with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code). + +
+
+ +Support level: **Compatible** + +[peewee](http://docs.peewee-orm.com/en/latest/) is a popular ORM framework for Python. To get all dependencies in your application, you can use the `pip install peewee==3.15.4` command. It is recommended to use peewee 3.15.4 or later versions. + +For an example of using peewee to build a TiDB application, see [Build a Simple CRUD App with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code). + +
+
+ After you have determined the driver or ORM, you can [connect to your TiDB cluster](https://docs.pingcap.com/tidbcloud/connect-to-tidb-cluster). diff --git a/develop/dev-guide-delete-data.md b/develop/dev-guide-delete-data.md index d89c923e58be..5dfb874bc756 100644 --- a/develop/dev-guide-delete-data.md +++ b/develop/dev-guide-delete-data.md @@ -141,6 +141,33 @@ func main() { +
+ +In Python, the example is as follows: + +```python +import MySQLdb +import datetime +import time +connection = MySQLdb.connect( + host="127.0.0.1", + port=4000, + user="root", + password="", + database="bookshop", + autocommit=True +) +with connection: + with connection.cursor() as cursor: + start_time = datetime.datetime(2022, 4, 15) + end_time = datetime.datetime(2022, 4, 15, 0, 15) + delete_sql = "DELETE FROM `bookshop`.`ratings` WHERE `rated_at` >= %s AND `rated_at` <= %s" + affect_rows = cursor.execute(delete_sql, (start_time, end_time)) + print(f'delete {affect_rows} data') +``` + +
+ @@ -315,6 +342,38 @@ In each iteration, `DELETE` deletes up to 1000 rows from `2022-04-15 00:00:00` t +
+ +In Python, the bulk-delete example is as follows: + +```python +import MySQLdb +import datetime +import time +connection = MySQLdb.connect( + host="127.0.0.1", + port=4000, + user="root", + password="", + database="bookshop", + autocommit=True +) +with connection: + with connection.cursor() as cursor: + start_time = datetime.datetime(2022, 4, 15) + end_time = datetime.datetime(2022, 4, 15, 0, 15) + affect_rows = -1 + while affect_rows != 0: + delete_sql = "DELETE FROM `bookshop`.`ratings` WHERE `rated_at` >= %s AND `rated_at` <= %s LIMIT 1000" + affect_rows = cursor.execute(delete_sql, (start_time, end_time)) + print(f'delete {affect_rows} data') + time.sleep(1) +``` + +In each iteration, `DELETE` deletes up to 1000 rows from `2022-04-15 00:00:00` to `2022-04-15 00:15:00`. + +
+ ## Non-transactional bulk-delete diff --git a/develop/dev-guide-insert-data.md b/develop/dev-guide-insert-data.md index 82edcee65072..b95183c99561 100644 --- a/develop/dev-guide-insert-data.md +++ b/develop/dev-guide-insert-data.md @@ -113,11 +113,11 @@ The following is a typical scenario of JDBC connection string configurations. In jdbc:mysql://127.0.0.1:4000/test?user=root&useConfigs=maxPerformance&useServerPrepStmts=true&prepStmtCacheSqlLimit=2048&prepStmtCacheSize=256&rewriteBatchedStatements=true&allowMultiQueries=true ``` -For a complete example in Java, see: +For complete examples in Java, see: -- [Build a Simple CRUD App with TiDB and Java - Using JDBC](/develop/dev-guide-sample-application-java.md#step-2-get-the-code) -- [Build a Simple CRUD App with TiDB and Java - Using Hibernate](/develop/dev-guide-sample-application-java.md#step-2-get-the-code) -- [Build the TiDB Application using Spring Boot](/develop/dev-guide-sample-application-spring-boot.md) +- [Build a simple CRUD application with TiDB and Java - using JDBC](/develop/dev-guide-sample-application-java.md#step-2-get-the-code) +- [Build a simple CRUD application with TiDB and Java - using Hibernate](/develop/dev-guide-sample-application-java.md#step-2-get-the-code) +- [Build the TiDB application using Spring Boot](/develop/dev-guide-sample-application-spring-boot.md) @@ -189,10 +189,40 @@ func buildBulkInsertSQL(amount int) string { } ``` -For a complete example in Golang, see: +For complete examples in Golang, see: -- [Use go-sql-driver/mysql to build a simple CRUD app with TiDB and Golang](/develop/dev-guide-sample-application-golang.md#step-2-get-the-code) -- [Use GORM to build a simple CRUD app with TiDB and Golang](/develop/dev-guide-sample-application-java.md#step-2-get-the-code) +- [Use go-sql-driver/mysql to build a simple CRUD application with TiDB and Golang](/develop/dev-guide-sample-application-golang.md#step-2-get-the-code) +- [Use GORM to build a simple CRUD application with TiDB and Golang](/develop/dev-guide-sample-application-java.md#step-2-get-the-code) + + + +
+ +```python +import MySQLdb +connection = MySQLdb.connect( + host="127.0.0.1", + port=4000, + user="root", + password="", + database="bookshop", + autocommit=True +) + +with get_connection(autocommit=True) as connection: + with connection.cursor() as cur: + player_list = random_player(1919) + for idx in range(0, len(player_list), 114): + cur.executemany("INSERT INTO player (id, coins, goods) VALUES (%s, %s, %s)", player_list[idx:idx + 114]) +``` + +For complete examples in Python, see: + +- [Use PyMySQL to build a simple CRUD application with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code) +- [Use mysqlclient to build a simple CRUD application with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code) +- [Use mysql-connector-python to build a simple CRUD application with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code) +- [Use SQLAlchemy to build a simple CRUD application with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code) +- [Use peewee to build a simple CRUD application with TiDB and Python](/develop/dev-guide-sample-application-python.md#step-2-get-the-code)
diff --git a/develop/dev-guide-optimistic-and-pessimistic-transaction.md b/develop/dev-guide-optimistic-and-pessimistic-transaction.md index d0bdf6b9664a..b20a2be9e3f7 100644 --- a/develop/dev-guide-optimistic-and-pessimistic-transaction.md +++ b/develop/dev-guide-optimistic-and-pessimistic-transaction.md @@ -91,6 +91,12 @@ func (tx *TiDBSqlTx) Rollback() error { +
+ +To ensure thread safety, you can use the mysqlclient driver to open multiple connections that are not shared between threads. + +
+ ### Write a pessimistic transaction example @@ -646,6 +652,187 @@ The Golang example already includes optimistic transactions. +
+ +```python +import time + +import MySQLdb +import os +import datetime +from threading import Thread + +REPEATABLE_ERROR_CODE_SET = { + 9007, # Transactions in TiKV encounter write conflicts. + 8028, # table schema changes + 8002, # "SELECT FOR UPDATE" commit conflict + 8022 # The transaction commit fails and has been rolled back +} + + +def create_connection(): + return MySQLdb.connect( + host="127.0.0.1", + port=4000, + user="root", + password="", + database="bookshop", + autocommit=False + ) + + +def prepare_data() -> None: + connection = create_connection() + with connection: + with connection.cursor() as cursor: + cursor.execute("INSERT INTO `books` (`id`, `title`, `type`, `published_at`, `price`, `stock`) " + "values (%s, %s, %s, %s, %s, %s)", + (1, "Designing Data-Intensive Application", "Science & Technology", + datetime.datetime(2018, 9, 1), 100, 10)) + + cursor.executemany("INSERT INTO `users` (`id`, `nickname`, `balance`) VALUES (%s, %s, %s)", + [(1, "Bob", 10000), (2, "ALICE", 10000)]) + connection.commit() + + +def buy_optimistic(thread_id: int, order_id: int, book_id: int, user_id: int, amount: int, + optimistic_retry_times: int = 5) -> None: + connection = create_connection() + + txn_log_header = f"/* txn {thread_id} */" + if thread_id != 1: + txn_log_header = "\t" + txn_log_header + + with connection: + with connection.cursor() as cursor: + cursor.execute("BEGIN OPTIMISTIC") + print(f'{txn_log_header} BEGIN OPTIMISTIC') + time.sleep(1) + + try: + # read the price of book + select_book_for_update = "SELECT `price`, `stock` FROM books WHERE id = %s FOR UPDATE" + cursor.execute(select_book_for_update, (book_id,)) + book = cursor.fetchone() + if book is None: + raise Exception("book_id not exist") + price, stock = book + print(f'{txn_log_header} {select_book_for_update} successful') + + if stock < amount: + raise Exception("book not enough, rollback") + + # update book + update_stock = "update `books` set stock = stock - %s where id = %s and stock - %s >= 0" + rows_affected = cursor.execute(update_stock, (amount, book_id, amount)) + print(f'{txn_log_header} {update_stock} successful') + + if rows_affected == 0: + raise Exception("stock not enough, rollback") + + # insert order + insert_order = "insert into `orders` (`id`, `book_id`, `user_id`, `quality`) values (%s, %s, %s, %s)" + cursor.execute(insert_order, (order_id, book_id, user_id, amount)) + print(f'{txn_log_header} {insert_order} successful') + + # update user + update_user = "update `users` set `balance` = `balance` - %s where id = %s" + cursor.execute(update_user, (amount * price, user_id)) + print(f'{txn_log_header} {update_user} successful') + + except Exception as err: + connection.rollback() + + print(f'something went wrong: {err}') + else: + # important here! you need deal the Exception from the TiDB + try: + connection.commit() + except MySQLdb.MySQLError as db_err: + code, desc = db_err.args + if code in REPEATABLE_ERROR_CODE_SET and optimistic_retry_times > 0: + print(f'retry, rest {optimistic_retry_times - 1} times, for {code} {desc}') + buy_optimistic(thread_id, order_id, book_id, user_id, amount, optimistic_retry_times - 1) + + +def buy_pessimistic(thread_id: int, order_id: int, book_id: int, user_id: int, amount: int) -> None: + connection = create_connection() + + txn_log_header = f"/* txn {thread_id} */" + if thread_id != 1: + txn_log_header = "\t" + txn_log_header + + with connection: + with connection.cursor() as cursor: + cursor.execute("BEGIN PESSIMISTIC") + print(f'{txn_log_header} BEGIN PESSIMISTIC') + time.sleep(1) + + try: + # read the price of book + select_book_for_update = "SELECT `price` FROM books WHERE id = %s FOR UPDATE" + cursor.execute(select_book_for_update, (book_id,)) + book = cursor.fetchone() + if book is None: + raise Exception("book_id not exist") + price = book[0] + print(f'{txn_log_header} {select_book_for_update} successful') + + # update book + update_stock = "update `books` set stock = stock - %s where id = %s and stock - %s >= 0" + rows_affected = cursor.execute(update_stock, (amount, book_id, amount)) + print(f'{txn_log_header} {update_stock} successful') + + if rows_affected == 0: + raise Exception("stock not enough, rollback") + + # insert order + insert_order = "insert into `orders` (`id`, `book_id`, `user_id`, `quality`) values (%s, %s, %s, %s)" + cursor.execute(insert_order, (order_id, book_id, user_id, amount)) + print(f'{txn_log_header} {insert_order} successful') + + # update user + update_user = "update `users` set `balance` = `balance` - %s where id = %s" + cursor.execute(update_user, (amount * price, user_id)) + print(f'{txn_log_header} {update_user} successful') + + except Exception as err: + connection.rollback() + print(f'something went wrong: {err}') + else: + connection.commit() + + +optimistic = os.environ.get('OPTIMISTIC') +alice = os.environ.get('ALICE') +bob = os.environ.get('BOB') + +if not (optimistic and alice and bob): + raise Exception("please use \"OPTIMISTIC= ALICE= " + "BOB= python3 txn_example.py\" to start this script") + +prepare_data() + +if bool(optimistic) is True: + buy_func = buy_optimistic +else: + buy_func = buy_pessimistic + +bob_thread = Thread(target=buy_func, kwargs={ + "thread_id": 1, "order_id": 1000, "book_id": 1, "user_id": 1, "amount": int(bob)}) +alice_thread = Thread(target=buy_func, kwargs={ + "thread_id": 2, "order_id": 1001, "book_id": 1, "user_id": 2, "amount": int(alice)}) + +bob_thread.start() +alice_thread.start() +bob_thread.join(timeout=10) +alice_thread.join(timeout=10) +``` + +The Python example already includes optimistic transactions. + +
+ ### An example that does not involve overselling @@ -672,6 +859,14 @@ go build -o bin/txn +
+ +```shell +OPTIMISTIC=False ALICE=4 BOB=6 python3 txn_example.py +``` + +
+ SQL logs: @@ -747,6 +942,14 @@ go build -o bin/txn +
+ +```shell +OPTIMISTIC=False ALICE=4 BOB=7 python3 txn_example.py +``` + +
+ ```sql @@ -983,6 +1186,12 @@ The Golang example in the [Write a pessimistic transaction example](#write-a-pes +
+ +The Python example in the [Write a pessimistic transaction example](#write-a-pessimistic-transaction-example) section already supports optimistic transactions and can be used directly without changes. + +
+ ### An example that does not involve overselling @@ -1009,6 +1218,14 @@ go build -o bin/txn +
+ +```shell +OPTIMISTIC=True ALICE=4 BOB=6 python3 txn_example.py +``` + +
+ SQL statement execution process: @@ -1092,6 +1309,14 @@ go build -o bin/txn +
+ +```shell +OPTIMISTIC=True ALICE=4 BOB=7 python3 txn_example.py +``` + +
+ ```sql diff --git a/develop/dev-guide-outdated-for-python-mysql-connector.md b/develop/dev-guide-outdated-for-python-mysql-connector.md deleted file mode 100644 index 55ef7307c77d..000000000000 --- a/develop/dev-guide-outdated-for-python-mysql-connector.md +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: App Development for mysql-connector-python -summary: Learn how to build a simple Python application based on TiDB and mysql-connector-python. ---- - -# App Development for the mysql-connector-python - -> **Note:** -> -> This legacy document is outdated and will not be updated thereafter. You can see [Developer Guide Overview](/develop/dev-guide-overview.md) for more details. - -This tutorial shows you how to build a simple Python application based on TiDB and mysql-connector-python. The sample application to build here is a simple CRM tool where you can add, query, and update customer and order information. - -## Step 1. Start a TiDB cluster - -Start a pseudo TiDB cluster on your local storage: - -```bash -docker run -p 127.0.0.1:$LOCAL_PORT:4000 pingcap/tidb:v5.1.0 -``` - -The above command starts a temporary and single-node cluster with mock TiKV. The cluster listens on the port `$LOCAL_PORT`. After the cluster is stopped, any changes already made to the database are not persisted. - -> **Note:** -> -> To deploy a "real" TiDB cluster for production, see the following guides: -> -> + [Deploy TiDB using TiUP for On-Premises](https://docs.pingcap.com/tidb/v5.1/production-deployment-using-tiup) -> + [Deploy TiDB on Kubernetes](https://docs.pingcap.com/tidb-in-kubernetes/stable) -> -> You can also [use TiDB Cloud](https://pingcap.com/products/tidbcloud/), a fully-managed Database-as-a-Service (DBaaS) of TiDB. - -## Step 2. Create a database - -1. In the SQL shell, create the `tidb_example` database that your application will use: - - {{< copyable "" >}} - - ```sql - CREATE DATABASE tidb_example; - ``` - -2. Create a SQL user for your application: - - {{< copyable "" >}} - - ```sql - CREATE USER IDENTIFIED BY ; - ``` - - Take note of the username and password. You will use them in your application code when initializing the project. - -3. Grant necessary permissions to the SQL user you have just created: - - {{< copyable "" >}} - - ```sql - GRANT ALL ON tidb_example.* TO ; - ``` - -## Step 3. Set virtual environments and initialize the project - -1. Use [Poetry](https://python-poetry.org/docs/), a dependency and package manager in Python, to set virtual environments and initialize the project. - - Poetry can isolate system dependencies from other dependencies and avoid dependency pollution. Use the following command to install Poetry. - - {{< copyable "" >}} - - ```bash - pip install --user poetry - ``` - -2. Initialize the development environment using Poetry: - - {{< copyable "" >}} - - ```bash - mkdir tidb_example - cd tidb_example - poetry init --no-interaction --dependency mysql-connector-python - ``` - -## Step 4. Get and run the application code - -The sample application code in this tutorial (`main.py`) uses mysql-connector-python to map Python methods to SQL operations that are described in the code comments. You can save the example application code as a Python file named `main.py` on your local machine. - -```python -import mysql.connector - -# Connects to the database in TiDB. -mydb = mysql.connector.connect( - host="localhost", - port="4000", - user="root", - passwd="", - database="tidb_example" -) - -# Creates the database cursor. -mycursor = mydb.cursor() - -# Created the orders and customer tables. -mycursor.execute("CREATE TABLE IF NOT EXISTS orders (oid INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, cid INT UNSIGNED, price FLOAT);") -mycursor.execute("CREATE TABLE IF NOT EXISTS customer (cid INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), gender ENUM ('Male', 'Female') NOT NULL)") - - -# Inserts data into the orders and customer tables. - -add_customer = ("INSERT INTO customer (name, gender) VALUES (%(name)s, %(gender)s);") -add_order = "INSERT INTO orders (cid, price) VALUES ({}, {});" - -data_customers = [ - {'name': 'Ben', 'gender': 'Male'}, - {'name': 'Alice', 'gender': 'Female'}, - {'name': 'Peter', 'gender': 'Male'}, -] - -data_orders = [ - [1.3, 4.0, 52.0, 123.0, 45.0], - [2.4, 23.4], - [100.0], -] - -# Inserts new employees. -for data_customer in data_customers: - mycursor.execute(add_customer, data_customer) - mydb.commit() - -cid = 1 -for price in data_orders[cid-1]: - mycursor.execute(add_order.format(cid, price)) - cid = cid + 1 - mydb.commit() - -# Queries the customer table. -mycursor.execute("SELECT * FROM customer") -myresult = mycursor.fetchall() -for x in myresult: - print(x) - -# Updates the orders table. -mycursor.execute("UPDATE orders SET price = %s WHERE oid = %s", (100.0, 1)) -mydb.commit() - -# Joins the two tables. -mycursor.execute("SELECT customer.name, orders.price FROM customer INNER JOIN orders ON customer.cid = orders.cid") - -myresult = mycursor.fetchall() -for x in myresult: - print(x) - -# Closes the database connection. -mycursor.close() -mydb.close() -``` - -### Step 1. Update the connection parameters and connect to TiDB - -Replace the string passed to `mysql.connector.connect()` with the connection string you have obtained when creating the database. - -```python -mydb = mysql.connector.connect( - host="localhost", - port=4000, - user="root", - passwd="", - database="tidb_example" -) -``` - -### Step 2. Run the application code - -Run the following command to run the `main.py` code: - -```python -python3 main.py -``` - -The expected output is as follows: - -``` -(1, 'Ben', 'Male') -(2, 'Alice', 'Female') -(3, 'Peter', 'Male') -('Ben', 100.0) -('Alice', 4.0) -('Peter', 52.0) -``` diff --git a/develop/dev-guide-outdated-for-sqlalchemy.md b/develop/dev-guide-outdated-for-sqlalchemy.md deleted file mode 100644 index 83d0aac552dd..000000000000 --- a/develop/dev-guide-outdated-for-sqlalchemy.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: App Development for SQLAlchemy -summary: Learn how to build a simple Python application based on TiDB and SQLAlchemy. ---- - -# App Development for SQLAlchemy - -> **Note:** -> -> This legacy document is outdated and will not be updated thereafter. You can see [Developer Guide Overview](/develop/dev-guide-overview.md) for more details. - -This tutorial shows you how to build a simple Python application based on TiDB and SQLAlchemy. The sample application to build here is a simple CRM tool where you can add, query, and update customer and order information. - -## Step 1. Start a TiDB cluster - -Start a pseudo TiDB cluster on your local storage: - -```bash -docker run -p 127.0.0.1:$LOCAL_PORT:4000 pingcap/tidb:v5.1.0 -``` - -The above command starts a temporary and single-node cluster with mock TiKV. The cluster listens on the port `$LOCAL_PORT`. After the cluster is stopped, any changes already made to the database are not persisted. - -> **Note:** -> -> To deploy a "real" TiDB cluster for production, see the following guides: -> -> + [Deploy TiDB using TiUP for On-Premises](https://docs.pingcap.com/tidb/v5.1/production-deployment-using-tiup) -> + [Deploy TiDB on Kubernetes](https://docs.pingcap.com/tidb-in-kubernetes/stable) -> -> You can also [use TiDB Cloud](https://pingcap.com/products/tidbcloud/), a fully-managed Database-as-a-Service (DBaaS) of TiDB. - -## Step 2. Create a database - -1. In the SQL shell, create the `test_sqlalchemy` database that your application will use: - - {{< copyable "" >}} - - ```sql - CREATE DATABASE test_sqlalchemy; - ``` - -2. Create a SQL user for your application: - - {{< copyable "" >}} - - ```sql - CREATE USER IDENTIFIED BY ; - ``` - - Take note of the username and password. You will use them in your application code when initializing the project. - -3. Grant necessary permissions to the SQL user you have just created: - - {{< copyable "" >}} - - ```sql - GRANT ALL ON test_sqlalchemy.* TO ; - ``` - -## Step 3. Set virtual environments and initialize the project - -1. Use [Poetry](https://python-poetry.org/docs/), a dependency and package manager in Python, to set virtual environments and initialize the project. - - Poetry can isolate system dependencies from other dependencies and avoid dependency pollution. Use the following command to install Poetry. - - {{< copyable "" >}} - - ```bash - pip install --user poetry - ``` - -2. Initialize the development environment using Poetry: - - {{< copyable "" >}} - - ```bash - poetry init --no-interaction --dependency sqlalchemy - - poetry add git+https://github.com/pingcap/sqlalchemy-tidb.git#main - ``` - -## Step 4. Get and run the application code - -The sample application code in this tutorial (`main.py`) uses SQLAlchemy to map Python methods to SQL operations. You can save the example application code as a Python file named `main.py` on your local machine. - -The code performs the following operations: - -1. Creates the `users` and `orders` tables in the `test_sqlalchemy` database as specified by the `User` and `Order` mapping classes. -2. Inserts data to the `users` and `orders` tables. -3. Deletes data from orders by `oid`. -4. Updates `orders` by `oid`. -5. Joins the `users` and `orders` tables. -6. Queries the `users` and `orders` tables using the same `uid`. - -```python -from sqlalchemy import Column, Integer, String, Float, ForeignKey, create_engine, Enum -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker - -import enum -engine = create_engine( - 'tidb://{username}:{password}@{hostname}:{port}/test_sqlalchemy?charset=utf8mb4', - echo=False) - -# The base class on which the objects will be defined. -Base = declarative_base() - -class Gender(enum.Enum): - Female = 1 - Male = 2 - -class User(Base): - __tablename__ = 'users' - uid = Column(Integer, primary_key=True) - name = Column(String(50)) - gender = Column(Enum(Gender)) - - def __repr__(self): - return "" % ( - self.name, self.gender) - -class Order(Base): - __tablename__ = 'orders' - - # Every SQLAlchemy table should have a primary key named 'id'. - oid = Column(Integer, primary_key=True, autoincrement=True) - - uid = Column(Integer) - price = Column(Float) - - # Prints out a user object conveniently. - def __repr__(self): - return "" % ( - self.name, self.uid, self.price) - -# Creates all tables by issuing CREATE TABLE commands to the database. -Base.metadata.create_all(engine) - -# Creates a new session to the database by using the described engine. -Session = sessionmaker(bind=engine) -session = Session() - -# Inserts users into the database. -session.add_all([ - User(name='Alice', gender=Gender.Female), - User(name='Peter', gender=Gender.Male), - User(name='Ben', gender=Gender.Male), -]) -session.commit() - -# Inserts Order into the database. -ed_user = Order(uid=1, price=2.5) - -# Adds the created users to the DB and commit. -session.add(ed_user) -session.commit() - -# Inserts Orders into the database. -session.add_all([ - Order(uid=1, price=0.5), - Order(uid=2, price=4.5), - Order(uid=2, price=2123.87), - Order(uid=3, price=212.5), - Order(uid=3, price=8.5), -] -) -session.commit() - -# Deletes orders by oid. -session.query(Order).filter(Order.oid == 4).delete() -session.commit() - -# Updates orders. -session.query(Order).filter(Order.oid == 1).update({'price': 3.5}) -session.commit() - -# Joins orders and users tables. -print( - session.query(User.name, Order.price) - .select_from(User) - .filter(User.uid == Order.uid) - .filter(Order.uid == 3) - .all() -) -``` - -### Step 1. Update the connection parameters and connect to TiDB - -In the `main.py` file above, replace the string passed to `create_engine()` with the connection string you have obtained when creating the database. - -```python -engine = create_engine( - 'tidb://{username}:{password}@{hostname}:{port}/test_sqlalchemy?charset=utf8mb4', - echo=False) -``` - -By default, you can set the string as follows: - -```python -engine = create_engine( - 'tidb://root:@127.0.0.1:4000/test_sqlalchemy?charset=utf8mb4', - echo=False) -``` - -### Step 2. Run the application code - -After the connection string is correctly set, run the application code: - -```bash -python3 main.py -``` - -The expected output is as follows: - -``` -[('Ben', 212.5), ('Ben', 8.5)] -``` diff --git a/develop/dev-guide-sample-application-python.md b/develop/dev-guide-sample-application-python.md new file mode 100644 index 000000000000..326735ae3f56 --- /dev/null +++ b/develop/dev-guide-sample-application-python.md @@ -0,0 +1,1103 @@ +--- +title: Build a Simple CRUD App with TiDB and Golang +summary: Learn how to build a simple CRUD application with TiDB and Golang. +aliases: ['/tidb/dev/dev-guide-outdated-for-python-mysql-connector','/tidb/dev/dev-guide-outdated-for-sqlalchemy'] +--- + + + + +# Build a Simple CRUD App with TiDB and Python + +This document describes how to use TiDB and Python to build a simple CRUD application. + +> **Note:** +> +> It is recommended to use Python 3.10 or a later Python version. + +## Step 1. Launch your TiDB cluster + + + +The following introduces how to start a TiDB cluster. + +**Use a TiDB Cloud Serverless Tier cluster** + +For detailed steps, see [Create a Serverless Tier cluster](/develop/dev-guide-build-cluster-in-cloud.md#step-1-create-a-serverless-tier-cluster). + +**Use a local cluster** + +For detailed steps, see [Deploy a local test cluster](/quick-start-with-tidb.md#deploy-a-local-test-cluster) or [Deploy a TiDB cluster using TiUP](/production-deployment-using-tiup.md). + + + + + +See [Create a Serverless Tier cluster](/develop/dev-guide-build-cluster-in-cloud.md#step-1-create-a-serverless-tier-cluster). + + + +## Step 2. Get the code + +```shell +git clone https://github.com/pingcap-inc/tidb-example-python.git +``` + + + +
+ +[SQLAlchemy](https://www.sqlalchemy.org/) is a popular open-source ORM library for Python. The following uses SQLAlchemy 1.44 as an example. + +```python +import uuid +from typing import List + +from sqlalchemy import create_engine, String, Column, Integer, select, func +from sqlalchemy.orm import declarative_base, sessionmaker + +engine = create_engine('mysql://root:@127.0.0.1:4000/test') +Base = declarative_base() +Base.metadata.create_all(engine) +Session = sessionmaker(bind=engine) + + +class Player(Base): + __tablename__ = "player" + + id = Column(String(36), primary_key=True) + coins = Column(Integer) + goods = Column(Integer) + + def __repr__(self): + return f'Player(id={self.id!r}, coins={self.coins!r}, goods={self.goods!r})' + + +def random_player(amount: int) -> List[Player]: + players = [] + for _ in range(amount): + players.append(Player(id=uuid.uuid4(), coins=10000, goods=10000)) + + return players + + +def simple_example() -> None: + with Session() as session: + # create a player, who has a coin and a goods. + session.add(Player(id="test", coins=1, goods=1)) + + # get this player, and print it. + get_test_stmt = select(Player).where(Player.id == "test") + for player in session.scalars(get_test_stmt): + print(player) + + # create players with bulk inserts. + # insert 1919 players totally, with 114 players per batch. + # each player has a random UUID + player_list = random_player(1919) + for idx in range(0, len(player_list), 114): + session.bulk_save_objects(player_list[idx:idx + 114]) + + # print the number of players + count = session.query(func.count(Player.id)).scalar() + print(f'number of players: {count}') + + # print 3 players. + three_players = session.query(Player).limit(3).all() + for player in three_players: + print(player) + + session.commit() + + +def trade_check(session: Session, sell_id: str, buy_id: str, amount: int, price: int) -> bool: + # sell player goods check + sell_player = session.query(Player.goods).filter(Player.id == sell_id).with_for_update().one() + if sell_player.goods < amount: + print(f'sell player {sell_id} goods not enough') + return False + + # buy player coins check + buy_player = session.query(Player.coins).filter(Player.id == buy_id).with_for_update().one() + if buy_player.coins < price: + print(f'buy player {buy_id} coins not enough') + return False + + +def trade(sell_id: str, buy_id: str, amount: int, price: int) -> None: + with Session() as session: + if trade_check(session, sell_id, buy_id, amount, price) is False: + return + + # deduct the goods of seller, and raise his/her the coins + session.query(Player).filter(Player.id == sell_id). \ + update({'goods': Player.goods - amount, 'coins': Player.coins + price}) + # deduct the coins of buyer, and raise his/her the goods + session.query(Player).filter(Player.id == buy_id). \ + update({'goods': Player.goods + amount, 'coins': Player.coins - price}) + + session.commit() + print("trade success") + + +def trade_example() -> None: + with Session() as session: + # create two players + # player 1: id is "1", has only 100 coins. + # player 2: id is "2", has 114514 coins, and 20 goods. + session.add(Player(id="1", coins=100, goods=0)) + session.add(Player(id="2", coins=114514, goods=20)) + session.commit() + + # player 1 wants to buy 10 goods from player 2. + # it will cost 500 coins, but player 1 cannot afford it. + # so this trade will fail, and nobody will lose their coins or goods + trade(sell_id="2", buy_id="1", amount=10, price=500) + + # then player 1 has to reduce the incoming quantity to 2. + # this trade will be successful + trade(sell_id="2", buy_id="1", amount=2, price=100) + + with Session() as session: + traders = session.query(Player).filter(Player.id.in_(("1", "2"))).all() + for player in traders: + print(player) + session.commit() + + +simple_example() +trade_example() +``` + +Compared with using drivers directly, SQLAlchemy provides an abstraction for the specific details of different databases when you create a database connection. In addition, SQLAlchemy encapsulates some operations such as session management and CRUD of basic objects, which greatly simplifies the code. + +The `Player` class is a mapping of a table to attributes in the application. Each attribute of `Player` corresponds to a field in the `player` table. To provide SQLAlchemy with more information, the attribute is defined as `id = Column(String(36), primary_key=True)` to indicate the field type and its additional attributes. For example, `id = Column(String(36), primary_key=True)` indicates that the `id` attribute is `String` type, the corresponding field in database is `VARCHAR` type, the length is `36`, and it is a primary key. + +For more information about how to use SQLAlchemy, refer to [SQLAlchemy documentation](https://www.sqlalchemy.org/). + +
+ +
+ +[peewee](http://docs.peewee-orm.com/en/latest/) is a popular open-source ORM library for Python. The following uses peewee 3.15.4 as an example. + +```python +import os +import uuid +from typing import List + +from peewee import * + +from playhouse.db_url import connect + +db = connect('mysql://root:@127.0.0.1:4000/test') + + +class Player(Model): + id = CharField(max_length=36, primary_key=True) + coins = IntegerField() + goods = IntegerField() + + class Meta: + database = db + table_name = "player" + + +def random_player(amount: int) -> List[Player]: + players = [] + for _ in range(amount): + players.append(Player(id=uuid.uuid4(), coins=10000, goods=10000)) + + return players + + +def simple_example() -> None: + # create a player, who has a coin and a goods. + Player.create(id="test", coins=1, goods=1) + + # get this player, and print it. + test_player = Player.select().where(Player.id == "test").get() + print(f'id:{test_player.id}, coins:{test_player.coins}, goods:{test_player.goods}') + + # create players with bulk inserts. + # insert 1919 players totally, with 114 players per batch. + # each player has a random UUID + player_list = random_player(1919) + Player.bulk_create(player_list, 114) + + # print the number of players + count = Player.select().count() + print(f'number of players: {count}') + + # print 3 players. + three_players = Player.select().limit(3) + for player in three_players: + print(f'id:{player.id}, coins:{player.coins}, goods:{player.goods}') + + +def trade_check(sell_id: str, buy_id: str, amount: int, price: int) -> bool: + sell_goods = Player.select(Player.goods).where(Player.id == sell_id).get().goods + if sell_goods < amount: + print(f'sell player {sell_id} goods not enough') + return False + + buy_coins = Player.select(Player.coins).where(Player.id == buy_id).get().coins + if buy_coins < price: + print(f'buy player {buy_id} coins not enough') + return False + + return True + + +def trade(sell_id: str, buy_id: str, amount: int, price: int) -> None: + with db.atomic() as txn: + try: + if trade_check(sell_id, buy_id, amount, price) is False: + txn.rollback() + return + + # deduct the goods of seller, and raise his/her the coins + Player.update(goods=Player.goods - amount, coins=Player.coins + price).where(Player.id == sell_id).execute() + # deduct the coins of buyer, and raise his/her the goods + Player.update(goods=Player.goods + amount, coins=Player.coins - price).where(Player.id == buy_id).execute() + + except Exception as err: + txn.rollback() + print(f'something went wrong: {err}') + else: + txn.commit() + print("trade success") + + +def trade_example() -> None: + # create two players + # player 1: id is "1", has only 100 coins. + # player 2: id is "2", has 114514 coins, and 20 goods. + Player.create(id="1", coins=100, goods=0) + Player.create(id="2", coins=114514, goods=20) + + # player 1 wants to buy 10 goods from player 2. + # it will cost 500 coins, but player 1 cannot afford it. + # so this trade will fail, and nobody will lose their coins or goods + trade(sell_id="2", buy_id="1", amount=10, price=500) + + # then player 1 has to reduce the incoming quantity to 2. + # this trade will be successful + trade(sell_id="2", buy_id="1", amount=2, price=100) + + # let's take a look for player 1 and player 2 currently + after_trade_players = Player.select().where(Player.id.in_(["1", "2"])) + for player in after_trade_players: + print(f'id:{player.id}, coins:{player.coins}, goods:{player.goods}') + + +db.connect() + +# recreate the player table +db.drop_tables([Player]) +db.create_tables([Player]) + +simple_example() +trade_example() +``` + +Compared with using drivers directly, peewee provides an abstraction for the specific details of different databases when you create a database connection. In addition, peewee encapsulates some operations such as session management and CRUD of basic objects, which greatly simplifies the code. + +The `Player` class is a mapping of a table to attributes in the application. Each attribute of `Player` corresponds to a field in the `player` table. To provide SQLAlchemy with more information, the attribute is defined as `id = Column(String(36), primary_key=True)` to indicate the field type and its additional attributes. For example, `id = Column(String(36), primary_key=True)` indicates that the `id` attribute is `String` type, the corresponding field in database is `VARCHAR` type, the length is `36`, and it is a primary key. + +For more information about how to use peewee, refer to [peewee documentation](http://docs.peewee-orm.com/en/latest/). + +
+ +
+ +[mysqlclient](https://pypi.org/project/mysqlclient/) is a popular open-source driver for Python. The following uses mysqlclient 2.1.1 as an example. Drivers for Python are more convenient to use than other languages, but they do not shield the underlying implementation and require manual management of transactions. If there are not a lot of scenarios where SQL is required, it is recommended to use ORM, which can help reduce the coupling of your program. + +```python +import uuid +from typing import List + +import MySQLdb +from MySQLdb import Connection +from MySQLdb.cursors import Cursor + +def get_connection(autocommit: bool = True) -> MySQLdb.Connection: + return MySQLdb.connect( + host="127.0.0.1", + port=4000, + user="root", + password="", + database="test", + autocommit=autocommit + ) + + +def create_player(cursor: Cursor, player: tuple) -> None: + cursor.execute("INSERT INTO player (id, coins, goods) VALUES (%s, %s, %s)", player) + + +def get_player(cursor: Cursor, player_id: str) -> tuple: + cursor.execute("SELECT id, coins, goods FROM player WHERE id = %s", (player_id,)) + return cursor.fetchone() + + +def get_players_with_limit(cursor: Cursor, limit: int) -> List[tuple]: + cursor.execute("SELECT id, coins, goods FROM player LIMIT %s", (limit,)) + return cursor.fetchall() + + +def random_player(amount: int) -> List[tuple]: + players = [] + for _ in range(amount): + players.append((uuid.uuid4(), 10000, 10000)) + + return players + + +def bulk_create_player(cursor: Cursor, players: List[tuple]) -> None: + cursor.executemany("INSERT INTO player (id, coins, goods) VALUES (%s, %s, %s)", players) + + +def get_count(cursor: Cursor) -> None: + cursor.execute("SELECT count(*) FROM player") + return cursor.fetchone()[0] + + +def trade_check(cursor: Cursor, sell_id: str, buy_id: str, amount: int, price: int) -> bool: + get_player_with_lock_sql = "SELECT coins, goods FROM player WHERE id = %s FOR UPDATE" + + # sell player goods check + cursor.execute(get_player_with_lock_sql, (sell_id,)) + _, sell_goods = cursor.fetchone() + if sell_goods < amount: + print(f'sell player {sell_id} goods not enough') + return False + + # buy player coins check + cursor.execute(get_player_with_lock_sql, (buy_id,)) + buy_coins, _ = cursor.fetchone() + if buy_coins < price: + print(f'buy player {buy_id} coins not enough') + return False + + +def trade_update(cursor: Cursor, sell_id: str, buy_id: str, amount: int, price: int) -> None: + update_player_sql = "UPDATE player set goods = goods + %s, coins = coins + %s WHERE id = %s" + + # deduct the goods of seller, and raise his/her the coins + cursor.execute(update_player_sql, (-amount, price, sell_id)) + # deduct the coins of buyer, and raise his/her the goods + cursor.execute(update_player_sql, (amount, -price, buy_id)) + + +def trade(connection: Connection, sell_id: str, buy_id: str, amount: int, price: int) -> None: + with connection.cursor() as cursor: + if trade_check(cursor, sell_id, buy_id, amount, price) is False: + connection.rollback() + return + + try: + trade_update(cursor, sell_id, buy_id, amount, price) + except Exception as err: + connection.rollback() + print(f'something went wrong: {err}') + else: + connection.commit() + print("trade success") + + +def simple_example() -> None: + with get_connection(autocommit=True) as conn: + with conn.cursor() as cur: + # create a player, who has a coin and a goods. + create_player(cur, ("test", 1, 1)) + + # get this player, and print it. + test_player = get_player(cur, "test") + print(f'id:{test_player[0]}, coins:{test_player[1]}, goods:{test_player[2]}') + + # create players with bulk inserts. + # insert 1919 players totally, with 114 players per batch. + # each player has a random UUID + player_list = random_player(1919) + for idx in range(0, len(player_list), 114): + bulk_create_player(cur, player_list[idx:idx + 114]) + + # print the number of players + count = get_count(cur) + print(f'number of players: {count}') + + # print 3 players. + three_players = get_players_with_limit(cur, 3) + for player in three_players: + print(f'id:{player[0]}, coins:{player[1]}, goods:{player[2]}') + + +def trade_example() -> None: + with get_connection(autocommit=False) as conn: + with conn.cursor() as cur: + # create two players + # player 1: id is "1", has only 100 coins. + # player 2: id is "2", has 114514 coins, and 20 goods. + create_player(cur, ("1", 100, 0)) + create_player(cur, ("2", 114514, 20)) + conn.commit() + + # player 1 wants to buy 10 goods from player 2. + # it will cost 500 coins, but player 1 cannot afford it. + # so this trade will fail, and nobody will lose their coins or goods + trade(conn, sell_id="2", buy_id="1", amount=10, price=500) + + # then player 1 has to reduce the incoming quantity to 2. + # this trade will be successful + trade(conn, sell_id="2", buy_id="1", amount=2, price=100) + + # let's take a look for player 1 and player 2 currently + with conn.cursor() as cur: + _, player1_coin, player1_goods = get_player(cur, "1") + print(f'id:1, coins:{player1_coin}, goods:{player1_goods}') + _, player2_coin, player2_goods = get_player(cur, "2") + print(f'id:2, coins:{player2_coin}, goods:{player2_goods}') + + +simple_example() +trade_example() +``` + +The driver has a lower level of encapsulation than ORM, so there are a lot of SQL statements in the program. Unlike ORM, there is no data object in drivers, so the `Player` queried by the driver is represented as a tuple. + +For more information about how to use mysqlclient, refer to [mysqlclient documentation](https://mysqlclient.readthedocs.io/). + +
+ +
+ +[PyMySQL](https://pypi.org/project/PyMySQL/) is a popular open-source driver for Python. The following uses PyMySQL 1.0.2 as an example. Drivers for Python are more convenient to use than other languages, but they do not shield the underlying implementation and require manual management of transactions. If there are not a lot of scenarios where SQL is required, it is recommended to use ORM, which can help reduce the coupling of your program. + +```python +import uuid +from typing import List + +import pymysql.cursors +from pymysql import Connection +from pymysql.cursors import DictCursor + + +def get_connection(autocommit: bool = False) -> Connection: + return pymysql.connect(host='127.0.0.1', + port=4000, + user='root', + password='', + database='test', + cursorclass=DictCursor, + autocommit=autocommit) + + +def create_player(cursor: DictCursor, player: tuple) -> None: + cursor.execute("INSERT INTO player (id, coins, goods) VALUES (%s, %s, %s)", player) + + +def get_player(cursor: DictCursor, player_id: str) -> dict: + cursor.execute("SELECT id, coins, goods FROM player WHERE id = %s", (player_id,)) + return cursor.fetchone() + + +def get_players_with_limit(cursor: DictCursor, limit: int) -> tuple: + cursor.execute("SELECT id, coins, goods FROM player LIMIT %s", (limit,)) + return cursor.fetchall() + + +def random_player(amount: int) -> List[tuple]: + players = [] + for _ in range(amount): + players.append((uuid.uuid4(), 10000, 10000)) + + return players + + +def bulk_create_player(cursor: DictCursor, players: List[tuple]) -> None: + cursor.executemany("INSERT INTO player (id, coins, goods) VALUES (%s, %s, %s)", players) + + +def get_count(cursor: DictCursor) -> int: + cursor.execute("SELECT count(*) as count FROM player") + return cursor.fetchone()['count'] + + +def trade_check(cursor: DictCursor, sell_id: str, buy_id: str, amount: int, price: int) -> bool: + get_player_with_lock_sql = "SELECT coins, goods FROM player WHERE id = %s FOR UPDATE" + + # sell player goods check + cursor.execute(get_player_with_lock_sql, (sell_id,)) + seller = cursor.fetchone() + if seller['goods'] < amount: + print(f'sell player {sell_id} goods not enough') + return False + + # buy player coins check + cursor.execute(get_player_with_lock_sql, (buy_id,)) + buyer = cursor.fetchone() + if buyer['coins'] < price: + print(f'buy player {buy_id} coins not enough') + return False + + +def trade_update(cursor: DictCursor, sell_id: str, buy_id: str, amount: int, price: int) -> None: + update_player_sql = "UPDATE player set goods = goods + %s, coins = coins + %s WHERE id = %s" + + # deduct the goods of seller, and raise his/her the coins + cursor.execute(update_player_sql, (-amount, price, sell_id)) + # deduct the coins of buyer, and raise his/her the goods + cursor.execute(update_player_sql, (amount, -price, buy_id)) + + +def trade(connection: Connection, sell_id: str, buy_id: str, amount: int, price: int) -> None: + with connection.cursor() as cursor: + if trade_check(cursor, sell_id, buy_id, amount, price) is False: + connection.rollback() + return + + try: + trade_update(cursor, sell_id, buy_id, amount, price) + except Exception as err: + connection.rollback() + print(f'something went wrong: {err}') + else: + connection.commit() + print("trade success") + + +def simple_example() -> None: + with get_connection(autocommit=True) as connection: + with connection.cursor() as cur: + # create a player, who has a coin and a goods. + create_player(cur, ("test", 1, 1)) + + # get this player, and print it. + test_player = get_player(cur, "test") + print(test_player) + + # create players with bulk inserts. + # insert 1919 players totally, with 114 players per batch. + # each player has a random UUID + player_list = random_player(1919) + for idx in range(0, len(player_list), 114): + bulk_create_player(cur, player_list[idx:idx + 114]) + + # print the number of players + count = get_count(cur) + print(f'number of players: {count}') + + # print 3 players. + three_players = get_players_with_limit(cur, 3) + for player in three_players: + print(player) + + +def trade_example() -> None: + with get_connection(autocommit=False) as connection: + with connection.cursor() as cur: + # create two players + # player 1: id is "1", has only 100 coins. + # player 2: id is "2", has 114514 coins, and 20 goods. + create_player(cur, ("1", 100, 0)) + create_player(cur, ("2", 114514, 20)) + connection.commit() + + # player 1 wants to buy 10 goods from player 2. + # it will cost 500 coins, but player 1 cannot afford it. + # so this trade will fail, and nobody will lose their coins or goods + trade(connection, sell_id="2", buy_id="1", amount=10, price=500) + + # then player 1 has to reduce the incoming quantity to 2. + # this trade will be successful + trade(connection, sell_id="2", buy_id="1", amount=2, price=100) + + # let's take a look for player 1 and player 2 currently + with connection.cursor() as cur: + print(get_player(cur, "1")) + print(get_player(cur, "2")) + + +simple_example() +trade_example() +``` + +The driver has a lower level of encapsulation than ORM, so there are a lot of SQL statements in the program. Unlike ORM, there is no data object in drivers, so the `Player` queried by the driver is represented as a dictionary. + +For more information about how to use PyMySQL, refer to [PyMySQL documentation](https://pymysql.readthedocs.io/en/latest/). + +
+ +
+ +[mysql-connector-python](https://dev.mysql.com/doc/connector-python/en/) is a popular open-source driver for Python. The following uses mysql-connector-python 8.0.31 as an example. Drivers for Python are more convenient to use than other languages, but they do not shield the underlying implementation and require manual management of transactions. If there are not a lot of scenarios where SQL is required, it is recommended to use ORM, which can help reduce the coupling of your program. + +```python +import uuid +from typing import List + +from mysql.connector import connect, MySQLConnection +from mysql.connector.cursor import MySQLCursor + + +def get_connection(autocommit: bool = True) -> MySQLConnection: + connection = connect(host='127.0.0.1', + port=4000, + user='root', + password='', + database='test') + connection.autocommit = autocommit + return connection + + +def create_player(cursor: MySQLCursor, player: tuple) -> None: + cursor.execute("INSERT INTO player (id, coins, goods) VALUES (%s, %s, %s)", player) + + +def get_player(cursor: MySQLCursor, player_id: str) -> tuple: + cursor.execute("SELECT id, coins, goods FROM player WHERE id = %s", (player_id,)) + return cursor.fetchone() + + +def get_players_with_limit(cursor: MySQLCursor, limit: int) -> List[tuple]: + cursor.execute("SELECT id, coins, goods FROM player LIMIT %s", (limit,)) + return cursor.fetchall() + + +def random_player(amount: int) -> List[tuple]: + players = [] + for _ in range(amount): + players.append((str(uuid.uuid4()), 10000, 10000)) + + return players + + +def bulk_create_player(cursor: MySQLCursor, players: List[tuple]) -> None: + cursor.executemany("INSERT INTO player (id, coins, goods) VALUES (%s, %s, %s)", players) + + +def get_count(cursor: MySQLCursor) -> int: + cursor.execute("SELECT count(*) FROM player") + return cursor.fetchone()[0] + + +def trade_check(cursor: MySQLCursor, sell_id: str, buy_id: str, amount: int, price: int) -> bool: + get_player_with_lock_sql = "SELECT coins, goods FROM player WHERE id = %s FOR UPDATE" + + # sell player goods check + cursor.execute(get_player_with_lock_sql, (sell_id,)) + _, sell_goods = cursor.fetchone() + if sell_goods < amount: + print(f'sell player {sell_id} goods not enough') + return False + + # buy player coins check + cursor.execute(get_player_with_lock_sql, (buy_id,)) + buy_coins, _ = cursor.fetchone() + if buy_coins < price: + print(f'buy player {buy_id} coins not enough') + return False + + +def trade_update(cursor: MySQLCursor, sell_id: str, buy_id: str, amount: int, price: int) -> None: + update_player_sql = "UPDATE player set goods = goods + %s, coins = coins + %s WHERE id = %s" + + # deduct the goods of seller, and raise his/her the coins + cursor.execute(update_player_sql, (-amount, price, sell_id)) + # deduct the coins of buyer, and raise his/her the goods + cursor.execute(update_player_sql, (amount, -price, buy_id)) + + +def trade(connection: MySQLConnection, sell_id: str, buy_id: str, amount: int, price: int) -> None: + with connection.cursor() as cursor: + if trade_check(cursor, sell_id, buy_id, amount, price) is False: + connection.rollback() + return + + try: + trade_update(cursor, sell_id, buy_id, amount, price) + except Exception as err: + connection.rollback() + print(f'something went wrong: {err}') + else: + connection.commit() + print("trade success") + + +def simple_example() -> None: + with get_connection(autocommit=True) as connection: + with connection.cursor() as cur: + # create a player, who has a coin and a goods. + create_player(cur, ("test", 1, 1)) + + # get this player, and print it. + test_player = get_player(cur, "test") + print(f'id:{test_player[0]}, coins:{test_player[1]}, goods:{test_player[2]}') + + # create players with bulk inserts. + # insert 1919 players totally, with 114 players per batch. + # each player has a random UUID + player_list = random_player(1919) + for idx in range(0, len(player_list), 114): + bulk_create_player(cur, player_list[idx:idx + 114]) + + # print the number of players + count = get_count(cur) + print(f'number of players: {count}') + + # print 3 players. + three_players = get_players_with_limit(cur, 3) + for player in three_players: + print(f'id:{player[0]}, coins:{player[1]}, goods:{player[2]}') + + +def trade_example() -> None: + with get_connection(autocommit=False) as conn: + with conn.cursor() as cur: + # create two players + # player 1: id is "1", has only 100 coins. + # player 2: id is "2", has 114514 coins, and 20 goods. + create_player(cur, ("1", 100, 0)) + create_player(cur, ("2", 114514, 20)) + conn.commit() + + # player 1 wants to buy 10 goods from player 2. + # it will cost 500 coins, but player 1 cannot afford it. + # so this trade will fail, and nobody will lose their coins or goods + trade(conn, sell_id="2", buy_id="1", amount=10, price=500) + + # then player 1 has to reduce the incoming quantity to 2. + # this trade will be successful + trade(conn, sell_id="2", buy_id="1", amount=2, price=100) + + # let's take a look for player 1 and player 2 currently + with conn.cursor() as cur: + _, player1_coin, player1_goods = get_player(cur, "1") + print(f'id:1, coins:{player1_coin}, goods:{player1_goods}') + _, player2_coin, player2_goods = get_player(cur, "2") + print(f'id:2, coins:{player2_coin}, goods:{player2_goods}') + + +simple_example() +trade_example() +``` + +The driver has a lower level of encapsulation than ORM, so there are a lot of SQL statements in the program. Unlike ORM, there is no data object in drivers, so the `Player` queried by the driver is represented as a tuple. + +For more information about how to use mysql-connector-python, refer to [mysql-connector-python documentation](https://dev.mysql.com/doc/connector-python/en/). + +
+ +
+ +## Step 3. Run the code + +The following content introduces how to run the code step by step. + +### Step 3.1 Initialize table + +Before running the code, you need to initialize the table manually. If you are using a local TiDB cluster, you can run the following command: + + + +
+ +```shell +mysql --host 127.0.0.1 --port 4000 -u root < player_init.sql +``` + +
+ +
+ +```shell +mycli --host 127.0.0.1 --port 4000 -u root --no-warn < player_init.sql +``` + +
+ +
+ +If you are not using a local cluster, or have not installed a MySQL client, connect to your cluster using your preferred method (such as Navicat, DBeaver, or other GUI tools) and run the SQL statements in the `player_init.sql` file. + +### Step 3.2 Modify parameters for TiDB Cloud + +If you are using a TiDB Cloud Serverless Tier cluster, you need to provide your CA root path and replace `` in the following examples with your CA path. To get the CA root path on your system, refer to [Where is the CA root path on my system?](https://docs.pingcap.com/tidbcloud/secure-connections-to-serverless-tier-clusters#where-is-the-ca-root-path-on-my-system). + + + +
+ +If you are using a TiDB Cloud Serverless Tier cluster, modify the parameters of the `create_engine` function in `sqlalchemy_example.py`: + +```python +engine = create_engine('mysql://root:@127.0.0.1:4000/test') +``` + +Suppose that the password you set is `123456`, and the connection parameters you get from the cluster details page are the following: + +- Endpoint: `xxx.tidbcloud.com` +- Port: `4000` +- User: `2aEp24QWEDLqRFs.root` + +In this case, you can modify the `create_engine` as follows: + +```python +engine = create_engine('mysql://2aEp24QWEDLqRFs.root:123456@xxx.tidbcloud.com:4000/test', connect_args={ + "ssl_mode": "VERIFY_IDENTITY", + "ssl": { + "ca": "" + } +}) +``` + +
+ +
+ +If you are using a TiDB Cloud Serverless Tier cluster, modify the parameters of the `create_engine` function in `sqlalchemy_example.py`: + +```python +db = connect('mysql://root:@127.0.0.1:4000/test') +``` + +Suppose that the password you set is `123456`, and the connection parameters you get from the cluster details page are the following: + +- Endpoint: `xxx.tidbcloud.com` +- Port: `4000` +- User: `2aEp24QWEDLqRFs.root` + +In this case, you can modify the `connect` as follows: + +- When peewee uses PyMySQL as the driver: + + ```python + db = connect('mysql://2aEp24QWEDLqRFs.root:123456@xxx.tidbcloud.com:4000/test', + ssl_verify_cert=True, ssl_ca="") + ``` + +- When peewee uses mysqlclient as the driver: + + ```python + db = connect('mysql://2aEp24QWEDLqRFs.root:123456@xxx.tidbcloud.com:4000/test', + ssl_mode="VERIFY_IDENTITY", ssl={"ca": ""}) + ``` + +Because peewee will pass parameters to the driver, you need to pay attention to the usage type of the driver when using peewee. + +
+ +
+ +If you are using a TiDB Cloud Serverless Tier cluster, change the `get_connection` function in `mysqlclient_example.py`: + +```python +def get_connection(autocommit: bool = True) -> MySQLdb.Connection: + return MySQLdb.connect( + host="127.0.0.1", + port=4000, + user="root", + password="", + database="test", + autocommit=autocommit + ) +``` + +Suppose that the password you set is `123456`, and the connection parameters you get from the cluster details page are the following: + +- Endpoint: `xxx.tidbcloud.com` +- Port: `4000` +- User: `2aEp24QWEDLqRFs.root` + +In this case, you can modify the `get_connection` as follows: + +```python +def get_connection(autocommit: bool = True) -> MySQLdb.Connection: + return MySQLdb.connect( + host="xxx.tidbcloud.com", + port=4000, + user="2aEp24QWEDLqRFs.root", + password="123456", + database="test", + autocommit=autocommit, + ssl_mode="VERIFY_IDENTITY", + ssl={ + "ca": "" + } + ) +``` + +
+ +
+ +If you are using a TiDB Cloud Serverless Tier cluster, change the `get_connection` function in `pymysql_example.py`: + +```python +def get_connection(autocommit: bool = False) -> Connection: + return pymysql.connect(host='127.0.0.1', + port=4000, + user='root', + password='', + database='test', + cursorclass=DictCursor, + autocommit=autocommit) +``` + +Suppose that the password you set is `123456`, and the connection parameters you get from the cluster details page are the following: + +- Endpoint: `xxx.tidbcloud.com` +- Port: `4000` +- User: `2aEp24QWEDLqRFs.root` + +In this case, you can modify the `get_connection` as follows: + +```python +def get_connection(autocommit: bool = False) -> Connection: + return pymysql.connect(host='xxx.tidbcloud.com', + port=4000, + user='2aEp24QWEDLqRFs.root', + password='123546', + database='test', + cursorclass=DictCursor, + autocommit=autocommit, + ssl_ca='', + ssl_verify_cert=True, + ssl_verify_identity=True) +``` + +
+ +
+ +If you are using a TiDB Cloud Serverless Tier cluster, change the `get_connection` function in `mysql_connector_python_example.py`: + +```python +def get_connection(autocommit: bool = True) -> MySQLConnection: + connection = connect(host='127.0.0.1', + port=4000, + user='root', + password='', + database='test') + connection.autocommit = autocommit + return connection +``` + +Suppose that the password you set is `123456`, and the connection parameters you get from the cluster details page are the following: + +- Endpoint: `xxx.tidbcloud.com` +- Port: `4000` +- User: `2aEp24QWEDLqRFs.root` + +In this case, you can modify the `get_connection` as follows: + +```python +def get_connection(autocommit: bool = True) -> MySQLConnection: + connection = connect( + host="xxx.tidbcloud.com", + port=4000, + user="2aEp24QWEDLqRFs.root", + password="123456", + database="test", + autocommit=autocommit, + ssl_ca='', + ssl_verify_identity=True + ) + connection.autocommit = autocommit + return connection +``` + +
+ +
+ +### Step 3.3 Run the code + +Before running the code, use the following command to install dependencies: + +```bash +pip3 install -r requirement.txt +``` + +If you need to run the script multiple times, follow the [Table initialization](#step-31-initialize-table) section to initialize the table again before each run. + + + +
+ +```bash +python3 sqlalchemy_example.py +``` + +
+ +
+ +```bash +python3 peewee_example.py +``` + +
+ +
+ +```bash +python3 mysqlclient_example.py +``` + +
+ +
+ +```bash +python3 pymysql_example.py +``` + +
+ +
+ +```bash +python3 mysql_connector_python_example.py +``` + +
+ +
+ +## Step 4. Expected output + + + +
+ +[SQLAlchemy Expected Output](https://github.com/pingcap-inc/tidb-example-python/blob/main/Expected-Output.md#SQLAlchemy) + +
+ +
+ +[peewee Expected Output](https://github.com/pingcap-inc/tidb-example-python/blob/main/Expected-Output.md#peewee) + +
+ +
+ +[mysqlclient Expected Output](https://github.com/pingcap-inc/tidb-example-python/blob/main/Expected-Output.md#mysqlclient) + +
+ +
+ +[PyMySQL Expected Output](https://github.com/pingcap-inc/tidb-example-python/blob/main/Expected-Output.md#PyMySQL) + +
+ +
+ +[mysql-connector-python Expected Output](https://github.com/pingcap-inc/tidb-example-python/blob/main/Expected-Output.md#mysql-connector-python) + +
+ +
diff --git a/develop/dev-guide-third-party-support.md b/develop/dev-guide-third-party-support.md index 90ccafa24d6b..2591ce6acd4c 100644 --- a/develop/dev-guide-third-party-support.md +++ b/develop/dev-guide-third-party-support.md @@ -26,40 +26,252 @@ If you encounter problems when connecting to TiDB using the tools listed in this ## Driver -| Language | Driver | Latest tested version | Support level | TiDB adapter | Tutorial | -| - | - | - | - | - | - | -| C | [libmysqlclient](https://dev.mysql.com/doc/c-api/8.0/en/c-api-introduction.html) | 8.0 | Compatible | N/A | N/A | -| C#(.Net) | [MySQL Connector/NET](https://downloads.mysql.com/archives/c-net/) | 8.0 | Compatible | N/A | N/A | -| ODBC | [MySQL Connector/ODBC](https://downloads.mysql.com/archives/c-odbc/) | 8.0 | Compatible | N/A | N/A | -| Go | [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) | v1.6.0 | Full | N/A | [Build a Simple CRUD App with TiDB and Golang](/develop/dev-guide-sample-application-golang.md) | -| Java | [JDBC](https://dev.mysql.com/downloads/connector/j/) | 8.0 | Full | [pingcap/mysql-connector-j](/develop/dev-guide-choose-driver-or-orm.md#java-drivers)
[pingcap/tidb-loadbalance](/develop/dev-guide-choose-driver-or-orm.md#java-client-load-balancing) | [Build a Simple CRUD App with TiDB and Java](/develop/dev-guide-sample-application-java.md) | -| JavaScript | [mysql](https://github.com/mysqljs/mysql) | v2.18.1 | Compatible | N/A | N/A | -| PHP | [mysqlnd](https://dev.mysql.com/downloads/connector/php-mysqlnd/) | PHP 5.4+ | Compatible | N/A | N/A | -| Python | [MySQL Connector/Python](https://downloads.mysql.com/archives/c-python/) | 8.0 | Compatible | N/A | N/A | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageDriverLatest tested versionSupport levelTiDB adapterTutorial
Clibmysqlclient8.0CompatibleN/AN/A
C#(.Net)MySQL Connector/NET8.0CompatibleN/AN/A
ODBCMySQL Connector/ODBC8.0CompatibleN/AN/A
Gogo-sql-driver/mysqlv1.6.0FullN/ABuild a Simple CRUD App with TiDB and Golang
JavaJDBC8.0Full + + Build a Simple CRUD App with TiDB and Java
JavaScriptmysqlv2.18.1CompatibleN/AN/A
PHPmysqlndPHP 5.4+CompatibleN/AN/A
Pythonmysql-connector-python8.0CompatibleN/ABuild a Simple CRUD App with TiDB and Python
mysqlclient2.1.1CompatibleN/ABuild a Simple CRUD App with TiDB and Python
PyMySQL1.0.2CompatibleN/ABuild a Simple CRUD App with TiDB and Python
## ORM -| Language | ORM framework | Latest tested version | Support level | TiDB adapter | Tutorial | -| - | - | - | - | - | - | -| Go | [gorm](https://github.com/go-gorm/gorm) | v1.23.5 | Full | N/A | [Build a Simple CRUD App with TiDB and Golang](/develop/dev-guide-sample-application-golang.md) | -| Go | [beego](https://github.com/beego/beego) | v2.0.3 | Full | N/A | N/A | -| Go | [upper/db](https://github.com/upper/db) | v4.5.2 | Full | N/A | N/A | -| Go | [xorm](https://gitea.com/xorm/xorm) | v1.3.1 | Full | N/A | N/A | -| Go | [ent](https://github.com/ent/ent) | v0.11.0 | Compatible | N/A | N/A | -| Java | [Hibernate](https://hibernate.org/orm/) | 6.1.0.Final | Full | N/A | [Build a Simple CRUD App with TiDB and Java](/develop/dev-guide-sample-application-java.md) | -| Java | [MyBatis](https://mybatis.org/mybatis-3/) | v3.5.10 | Full | N/A | [Build a Simple CRUD App with TiDB and Java](/develop/dev-guide-sample-application-java.md) | -| Java | [Spring Data JPA](https://spring.io/projects/spring-data-jpa/) | 2.7.2 | Full | N/A | [Build a TiDB Application Using Spring Boot](/develop/dev-guide-sample-application-spring-boot.md) | -| Java | [jOOQ](https://github.com/jOOQ/jOOQ) | v3.16.7 (Open Source) | Full | N/A | N/A | -| Ruby | [Active Record](https://guides.rubyonrails.org/active_record_basics.html) | v7.0 | Full | N/A | N/A | -| JavaScript/TypeScript | [sequelize](https://www.npmjs.com/package/sequelize) | v6.20.1 | Compatible | N/A | N/A | -| JavaScript/TypeScript | [Knex.js](https://knexjs.org/) | v1.0.7 | Compatible | N/A | N/A | -| JavaScript/TypeScript | [Prisma Client](https://www.prisma.io/) | 3.15.1 | Compatible | N/A | N/A | -| JavaScript/TypeScript | [TypeORM](https://www.npmjs.com/package/typeorm) | v0.3.6 | Compatible | N/A | N/A | -| PHP | [laravel](https://laravel.com/) | v9.1.10 | Compatible | [laravel-tidb](https://github.com/colopl/laravel-tidb) | N/A | -| Python | [Django](https://pypi.org/project/Django/) | v4.0.5 | Compatible | [django-tidb](https://github.com/pingcap/django-tidb) | N/A | -| Python | [peewee](https://github.com/coleifer/peewee/) | v3.14.10 | Compatible | N/A | N/A | -| Python | [PonyORM](https://ponyorm.org/) | v0.7.16 | Compatible | N/A | N/A | -| Python | [SQLAlchemy](https://www.sqlalchemy.org/) | v1.4.37 | Compatible | N/A | N/A | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
LanguageORM frameworkLatest tested versionSupport levelTiDB adapterTutorial
Gogormv1.23.5FullN/ABuild a Simple CRUD App with TiDB and Golang
beegov2.0.3FullN/AN/A
upper/dbv4.5.2FullN/AN/A
xormv1.3.1FullN/AN/A
entv0.11.0CompatibleN/AN/A
JavaHibernate6.1.0.FinalFullN/ABuild a Simple CRUD App with TiDB and Java
MyBatisv3.5.10FullN/ABuild a Simple CRUD App with TiDB and Java
Spring Data JPA2.7.2FullN/ABuild a Simple CRUD App with TiDB and Spring Boot
jOOQv3.16.7 (Open Source)FullN/AN/A
RubyActive Recordv7.0FullN/AN/A
JavaScript / TypeScriptsequelizev6.20.1CompatibleN/AN/A
Knex.jsv1.0.7CompatibleN/AN/A
Prisma Client3.15.1CompatibleN/AN/A
TypeORMv0.3.6CompatibleN/AN/A
PHPlaravelv9.1.10Compatiblelaravel-tidbN/A
PythonDjangov4.0.5Compatibledjango-tidbN/A
peeweev3.14.10CompatibleN/ABuild a Simple CRUD App with TiDB and Python
SQLAlchemyv1.4.37CompatibleN/ABuild a Simple CRUD App with TiDB and Python
## GUI @@ -69,9 +281,38 @@ If you encounter problems when connecting to TiDB using the tools listed in this | [Navicat for MySQL](https://www.navicat.com/) | 16.0.14 | Compatible | N/A | | [MySQL Workbench](https://www.mysql.com/products/workbench/) | 8.0 | Compatible | N/A | -| IDE | Plugin | Support level | Tutorial | -| - | - | - | - | -| [DataGrip](https://www.jetbrains.com/datagrip/) | N/A | Compatible | N/A | -| [IntelliJ IDEA](https://www.jetbrains.com/idea/) | N/A | Compatible | N/A | -| [Visual Studio Code](https://code.visualstudio.com/) | [TiDE](https://marketplace.visualstudio.com/items?itemName=dragonly.ticode) | Compatible | N/A | -| [Visual Studio Code](https://code.visualstudio.com/) | [MySQL](https://marketplace.visualstudio.com/items?itemName=formulahendry.vscode-mysql) | Compatible | N/A | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IDEPluginSupport levelTutorial
DataGripN/ACompatibleN/A
IntelliJ IDEAN/ACompatibleN/A
Visual Studio CodeTiDECompatibleN/A
MySQLCompatibleN/A