使用 Django 构建 TiDB 应用程序

本文档将展示如何使用 Django 构建一个 TiDB Web 应用程序。使用 django-tidb 模块作为数据访问能力的框架。示例应用程序的代码可从 Github 下载。

这是一个较为完整的构建 Restful API 的示例应用程序,展示了一个使用 TiDB 作为数据库的通用 Django 后端服务。该示例设计了以下过程,用于还原一个现实场景:

coinsgoodsid

你可以以此示例为基础,构建自己的应用程序。

第 1 步:启动你的 TiDB 集群

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

使用 TiDB Serverless 集群

详细步骤,请参考:创建 TiDB Serverless 集群。

使用本地集群

详细步骤,请参考:部署本地测试 TiDB 集群或部署正式 TiDB 集群。

第 2 步:安装 Python

请在你的计算机上下载并安装 Python。本文的示例使用 Django 3.2.16 版本。根据 Django 文档,Django 3.2.16 版本支持 Python 3.6、3.7、3.8、3.9 和 3.10 版本,推荐使用 Python 3.10 版本。

第 3 步:获取应用程序代码

django_example

第 4 步:运行应用程序

python manage.py migratedjangoplayer

如果你想了解有关此应用程序的代码的详细信息,可参阅实现细节部分。

第 4 步第 1 部分:TiDB Cloud 更改参数

example_project/settings.pyDATABASES
DATABASES = {
    'default': {
        'ENGINE': 'django_tidb',
        'NAME': 'django',
        'USER': 'root',
        'PASSWORD': '',
        'HOST': '127.0.0.1',
        'PORT': 4000,
    },
}
123456
xxx.tidbcloud.com40002aEp24QWEDLqRFs.root

下面以 macOS 为例,应将参数更改为:

DATABASES = {
    'default': {
        'ENGINE': 'django_tidb',
        'NAME': 'django',
        'USER': '2aEp24QWEDLqRFs.root',
        'PASSWORD': '123456',
        'HOST': 'xxx.tidbcloud.com',
        'PORT': 4000,
        'OPTIONS': {
            'ssl': {
                "ca": "<ca_path>"
            },
        },
    },
}

第 4 步第 2 部分:运行

cd <path>/tidb-example-python
pip install -r requirement.txt
cd django_example
python manage.py migrate
python manage.py runserver

第 4 步第 3 部分:输出

输出的最后部分应如下所示:

Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
December 12, 2022 - 08:21:50
Django version 3.2.16, using settings 'example_project.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

如果你想了解有关此应用程序的代码的详细信息,可参阅实现细节部分。

第 5 步:HTTP 请求

http://localhost:8000
  • 使用 Postman(推荐)
  • 使用 curl
  • 使用 Shell 脚本

实现细节

本小节介绍示例应用程序项目中的组件。

总览

本示例项目的目录树大致如下所示:

.
├── example_project
│   ├── __init__.py
│   ├── asgi.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── player
│   ├── __init__.py
│   ├── admin.py
│   ├── apps.py
│   ├── migrations
│   │   ├── 0001_initial.py
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   ├── urls.py
│   └── views.py
└── manage.py

其中:

__init__.pymanage.pyexample_projectsettings.pyurls.pyplayerPlayerpython manage.py startapp playerplayermodels.pyPlayermigrationspython manage.py makemigrations playermodels.pyurls.pyviews.py

项目配置

example_projectsettings.pysettings.py
...

# Application definition

INSTALLED_APPS = [
    'player.apps.PlayerConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

...

# Database
# https://docs.djangoproject.com/en/3.2/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': 'django_tidb',
        'NAME': 'django',
        'USER': 'root',
        'PASSWORD': '',
        'HOST': '127.0.0.1',
        'PORT': 4000,
    },
}
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'

...

其中:

INSTALLED_APPSMIDDLEWARECsrfViewMiddlewareDATABASESENGINEdjango_tidb

根路由

example_projecturls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('player/', include('player.urls')),
    path('admin/', admin.site.urls),
]
player/player.urlsplayerurls.pyplayer/

player 应用

playerPlayer

数据模型

models.pyPlayer
from django.db import models

# Create your models here.


class Player(models.Model):
    id = models.AutoField(primary_key=True)
    coins = models.IntegerField()
    goods = models.IntegerField()

    objects = models.Manager()

    class Meta:
        db_table = "player"

    def as_dict(self):
        return {
            "id": self.id,
            "coins": self.coins,
            "goods": self.goods,
        }
Metadb_tableplayer
idcoinsgoods
idmodels.AutoField(primary_key=True)coinsmodels.IntegerField()goodsmodels.IntegerField()

关于数据模型的详细信息,可查看 Django 模型文档。

数据模型迁移

models.pyPlayerpython manage.py makemigrations playermigrations0001_initial.py
# Generated by Django 3.2.16 on 2022-11-16 11:09

from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Player',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('coins', models.IntegerField()),
                ('goods', models.IntegerField()),
            ],
            options={
                'db_table': 'player',
            },
        ),
    ]
python manage.py sqlmigrate ...python manage.py sqlmigrate player 0001
--
-- Create model Player
--
CREATE TABLE `player` (`id` integer AUTO_INCREMENT NOT NULL PRIMARY KEY, `coins` integer NOT NULL, `goods` integer NOT NULL);
python manage.py migrate

应用路由

player/player.urlsplayerurls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.create, name='create'),
    path('count', views.count, name='count'),
    path('limit/<int:limit>', views.limit_list, name='limit_list'),
    path('<int:player_id>', views.get_by_id, name='get_by_id'),
    path('trade', views.trade, name='trade'),
]

应用路由注册了 5 个路径:

''views.create'count'views.count'limit/'views.limit_listintintlimitlimit''views.get_by_id'trade'views.trade
player/
''http(s)://(:)/player'count'http(s)://(:)/player/count'limit/'limit3http(s)://(:)/player/limit/3

逻辑实现

playerviews.py
from django.db import transaction
from django.db.models import F
from django.shortcuts import get_object_or_404

from django.http import HttpResponse, JsonResponse
from django.views.decorators.http import *
from .models import Player
import json


@require_POST
def create(request):
    dict_players = json.loads(request.body.decode('utf-8'))
    players = list(map(
        lambda p: Player(
            coins=p['coins'],
            goods=p['goods']
        ), dict_players))
    result = Player.objects.bulk_create(objs=players)
    return HttpResponse(f'create {len(result)} players.')


@require_GET
def count(request):
    return HttpResponse(Player.objects.count())


@require_GET
def limit_list(request, limit: int = 0):
    if limit == 0:
        return HttpResponse("")
    players = set(Player.objects.all()[:limit])
    dict_players = list(map(lambda p: p.as_dict(), players))
    return JsonResponse(dict_players, safe=False)


@require_GET
def get_by_id(request, player_id: int):
    result = get_object_or_404(Player, pk=player_id).as_dict()
    return JsonResponse(result)


@require_POST
@transaction.atomic
def trade(request):
    sell_id, buy_id, amount, price = int(request.POST['sellID']), int(request.POST['buyID']), \
                                     int(request.POST['amount']), int(request.POST['price'])
    sell_player = Player.objects.select_for_update().get(id=sell_id)
    if sell_player.goods < amount:
        raise Exception(f'sell player {sell_player.id} goods not enough')

    buy_player = Player.objects.select_for_update().get(id=buy_id)
    if buy_player.coins < price:
        raise Exception(f'buy player {buy_player.id} coins not enough')

    Player.objects.filter(id=sell_id).update(goods=F('goods') - amount, coins=F('coins') + price)
    Player.objects.filter(id=buy_id).update(goods=F('goods') + amount, coins=F('coins') - price)

    return HttpResponse("trade successful")

下面将逐一解释代码中的重点部分:

dict_players = json.loads(request.body.decode('utf-8'))
players = list(map(
    lambda p: Player(
        coins=p['coins'],
        goods=p['goods']
    ), dict_players))
result = Player.objects.bulk_create(objs=players)
return HttpResponse(f'create {len(result)} players.')
if limit == 0:
    return HttpResponse("")
players = set(Player.objects.all()[:limit])
dict_players = list(map(lambda p: p.as_dict(), players))
return JsonResponse(dict_players, safe=False)
result = get_object_or_404(Player, pk=player_id).as_dict()
return JsonResponse(result)
sell_id, buy_id, amount, price = int(request.POST['sellID']), int(request.POST['buyID']), \
                                int(request.POST['amount']), int(request.POST['price'])
sell_player = Player.objects.select_for_update().get(id=sell_id)
if sell_player.goods < amount:
    raise Exception(f'sell player {sell_player.id} goods not enough')

buy_player = Player.objects.select_for_update().get(id=buy_id)
if buy_player.coins < price:
    raise Exception(f'buy player {buy_player.id} coins not enough')
Player.objects.filter(id=sell_id).update(goods=F('goods') - amount, coins=F('coins') + price)
Player.objects.filter(id=buy_id).update(goods=F('goods') + amount, coins=F('coins') - price)
return HttpResponse("trade successful")

创建相同依赖空白程序(可选)

django-admindjango_example
pip install -r requirement.txt
django-admin startproject copy_django_example
cd copy_django_example
DATABASES = {
    'default': {
        'ENGINE': 'django_tidb',
        'NAME': 'django',
        'USER': 'root',
        'PASSWORD': '',
        'HOST': '127.0.0.1',
        'PORT': 4000,
    },
}
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    # 'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

至此,你已经完成了一个空白的应用程序,此应用程序与示例应用程序的依赖完全相同。如果需要进一步了解 Django 的使用方法,参考: