Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Flask — это лёгкий веб-фреймворк на Python, который даёт полный контроль над структурой приложения. В сочетании с mssql-python можно создавать веб-приложения и REST API на базе Microsoft SQL и База данных SQL Azure с минимальными накладными расходами.
Необходимые условия
- Python 3.10 или более поздней версии.
- Пакеты
mssql-pythonиflask. Установите оба компонента с помощьюpip install flask mssql-python. - Установите единовременные предварительные условия для операционной системы. Пользователи Windows могут пропустить этот шаг. Для полной информации о платформе см. Установить mssql-python.
apk add libtool krb5-libs krb5-dev
Создание базы данных SQL
Создайте или подключитесь к SQL-базе данных на одной из следующих платформ:
В примерах этой статьи используется пример базы данных AdventureWorksLT, а именно таблица SalesLT.Product. Если у вас не установлен AdventureWorksLT, посмотрите примеры баз данных AdventureWorks.
Настройка проекта
Установка зависимостей
Установите необходимые пакеты с помощью pip:
pip install flask mssql-python
структура проекта
Организуйте свой проект с отдельными модулями для настройки, управления соединениями, маршрутов и тестов:
my_app/
├── app.py # Flask app and routes
├── config.py # database settings
├── database.py # connection lifecycle
├── test_app.py # pytest tests
└── blueprints/ # optional: routes grouped into modules
├── __init__.py
└── products.py
Управление подключением к базе данных
Flask не включает встроенный уровень базы данных, поэтому вы управляете соединениями напрямую. Паттерн в этом разделе хранит одно соединение на каждый запрос на объекте g Flask и автоматически закрывает его после окончания запроса.
Создайте config.py
Централизовать настройки базы данных в классе конфигурации. Переменные окружения позволяют отменять настройки по умолчанию без изменения кода.
# config.py
import os
class Config:
"""Application configuration."""
DATABASE_SERVER = os.getenv("DB_SERVER", "<server>.database.windows.net")
DATABASE_NAME = os.getenv("DB_NAME", "<database>")
POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "10"))
Создайте database.py
Модуль database.py управляет жизненным циклом подключения. Объект g во Flask — это пространство имён, отдельное для каждого запроса, поэтому хранение там соединения гарантирует, что каждый запрос получает собственное соединение, которое удаляется после завершения запроса.
Функция get_connection_string() строит строка подключения из конфигурации приложения. get_db() Функция создаёт соединение при первом вызове и повторно использует его для остальной части запроса.
close_db() Функция запускается автоматически в конце каждого запроса, откатывая транзакцию в случае исключения и совершая иное.
init_app() Функция регистрирует это поведение разбора с помощью приложения Flask.
# database.py
import mssql_python
from flask import g, current_app
def get_connection_string() -> str:
"""Build connection string from Flask app config."""
cfg = current_app.config
return (
f"Server={cfg['DATABASE_SERVER']};"
f"Database={cfg['DATABASE_NAME']};"
"Authentication=ActiveDirectoryDefault;"
"Encrypt=yes"
)
def get_db():
"""Get a database cursor for the current request.
The connection is stored on Flask's g object so it persists
for the duration of the request and is reused across calls.
"""
if "db_conn" not in g:
g.db_conn = mssql_python.connect(get_connection_string())
g.db_cursor = g.db_conn.cursor()
return g.db_cursor
def close_db(exception=None):
"""Close the database connection at the end of the request."""
cursor = g.pop("db_cursor", None)
conn = g.pop("db_conn", None)
if cursor is not None:
cursor.close()
if conn is not None:
if exception:
conn.rollback()
else:
conn.commit()
conn.close()
def init_app(app):
"""Register database teardown with the Flask app."""
app.teardown_appcontext(close_db)
Замечание
ActiveDirectoryDefault использует DefaultAzureCredential, который последовательно пробует несколько поставщиков учетных данных. Первое соединение может быть медленным, потому что SDK идёт по цепочке, пока не найдёт работающего провайдера. В продакшене, если вы знаете, какой тип учетных данных использует ваша среда, укажите его напрямую (например, ActiveDirectoryMSI для управляемой идентичности), чтобы избежать цепной ходьбы. Дополнительные сведения см. в разделе проверки подлинности Microsoft Entra.
Приложение Flask
Следующий пример показывает полноценное приложение Flask с маршрутами для размещения, извлечения, создания, обновления и удаления продуктов.
Создайте app.py
Модуль приложения создаёт приложение Flask, загружает конфигурацию и регистрирует разборку базы данных. Каждая функция маршрута вызывает get_db(), чтобы получить курсор, выполняет запросы с параметризованным SQL (используя %(name)s плейсхолдеры и словарь значений) и возвращает JSON-ответы.
# app.py
from flask import Flask, jsonify, request, abort
from config import Config
from database import init_app, get_db
app = Flask(__name__)
app.config.from_object(Config)
init_app(app)
@app.route("/")
def index():
return jsonify({"message": "Product API", "docs": "/products"})
@app.route("/products")
def list_products():
"""List products with pagination."""
page = request.args.get("page", 1, type=int)
page_size = request.args.get("page_size", 10, type=int)
skip = (page - 1) * page_size
cursor = get_db()
cursor.execute("SELECT COUNT(*) FROM SalesLT.Product")
total = cursor.fetchval()
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, ProductCategoryID
FROM SalesLT.Product
ORDER BY ProductID
OFFSET %(skip)s ROWS
FETCH NEXT %(limit)s ROWS ONLY
""", {"skip": skip, "limit": page_size})
items = [{
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
} for row in cursor.fetchall()]
return jsonify({
"items": items,
"total": total,
"page": page,
"page_size": page_size,
"pages": (total + page_size - 1) // page_size
})
@app.route("/products/<int:product_id>")
def get_product(product_id):
"""Get a single product by ID."""
cursor = get_db()
cursor.execute("""
SELECT ProductID, Name, ProductNumber, ListPrice, Color, ProductCategoryID
FROM SalesLT.Product
WHERE ProductID = %(id)s
""", {"id": product_id})
row = cursor.fetchone()
if not row:
abort(404)
return jsonify({
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
})
@app.route("/products", methods=["POST"])
def create_product():
"""Create a new product."""
data = request.get_json()
if not data:
abort(400)
cursor = get_db()
# OUTPUT INSERTED returns the new row's columns in the same statement,
# so you don't need a separate SELECT to get the generated ID and defaults.
# ProductNumber is required and unique. StandardCost and SellStartDate are
# also NOT NULL in SalesLT.Product, so supply values for them.
cursor.execute("""
INSERT INTO SalesLT.Product
(Name, ProductNumber, ListPrice, Color, Size, ProductCategoryID, StandardCost, SellStartDate)
OUTPUT INSERTED.ProductID, INSERTED.Name, INSERTED.ProductNumber, INSERTED.ListPrice,
INSERTED.Color, INSERTED.ProductCategoryID
VALUES (%(name)s, %(product_number)s, %(price)s, %(color)s, %(size)s, %(category_id)s, 0, GETDATE())
""", {
"name": data["name"],
"product_number": data["product_number"],
"price": data["price"],
"color": data.get("color"),
"size": data.get("size"),
"category_id": data["category_id"]
})
row = cursor.fetchone()
return jsonify({
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
}), 201
@app.route("/products/<int:product_id>", methods=["PUT"])
def update_product(product_id):
"""Update an existing product."""
data = request.get_json()
if not data:
abort(400)
cursor = get_db()
updates = []
params = {"id": product_id}
for field in ("name", "product_number", "price", "color", "category_id"):
if field in data:
col = {"name": "Name", "product_number": "ProductNumber",
"price": "ListPrice", "color": "Color",
"category_id": "ProductCategoryID"}[field]
updates.append(f"{col} = %({field})s")
params[field] = data[field]
if not updates:
abort(400)
cursor.execute(f"""
UPDATE SalesLT.Product SET {', '.join(updates)}
OUTPUT INSERTED.ProductID, INSERTED.Name, INSERTED.ProductNumber, INSERTED.ListPrice,
INSERTED.Color, INSERTED.ProductCategoryID
WHERE ProductID = %(id)s
""", params)
row = cursor.fetchone()
if not row:
abort(404)
return jsonify({
"id": row.ProductID,
"name": row.Name,
"product_number": row.ProductNumber,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
})
@app.route("/products/<int:product_id>", methods=["DELETE"])
def delete_product(product_id):
"""Delete a product."""
cursor = get_db()
cursor.execute("DELETE FROM SalesLT.Product WHERE ProductID = %(id)s", {"id": product_id})
if cursor.rowcount == 0:
abort(404)
return "", 204
@app.route("/health")
def health_check():
"""Check database connectivity."""
try:
cursor = get_db()
cursor.execute("SELECT 1")
return jsonify({"status": "healthy", "database": "connected"})
except Exception as e:
return jsonify({"status": "unhealthy", "error": str(e)}), 503
Запуск приложения
Запустите сервер разработки:
flask --app app run --debug --port 5000
Сервер прослушивает http://localhost:5000. Откройте второй терминал и обратитесь к конечным точкам с помощью curl, чтобы убедиться, что приложение взаимодействует с вашей базой данных:
# Check database connectivity
curl http://localhost:5000/health
# List the first page of products
curl "http://localhost:5000/products?page_size=5"
# Get a single product by ID
curl http://localhost:5000/products/680
Замечание
В PowerShell curl — это псевдоним для Invoke-WebRequest. Простые команды GET здесь работают нормально, но ответ возвращается в виде объекта, а не печатного JSON. Команды, использующие curl флаги вроде -X, -H, или -d (как в POST следующем примере), работают не так, как написано. В Windows curl.exe используйте для выполнения команд точно так, как показано, или используйте PowerShell Invoke-RestMethod (напримерInvoke-RestMethod http://localhost:5000/health), который также анализирует ответ JSON.
Каждая конечная точка возвращает JSON. Вы также можете открыть http://localhost:5000/products в браузере, чтобы просмотреть список с разбивкой на страницы.
Пулинг соединений
Без пула соединений каждый запрос открывает и закрывает TCP-соединение с Microsoft SQL, что добавляет задержку. Механизм пула соединений поддерживает набор незанятых соединений, готовых к повторному использованию. Чтобы включить использование пула соединений, вызовите mssql_python.pooling() один раз на уровне модуля. При включённом пуле conn.close()close_db при разборе соединение возвращается к пулу, а не закрывает его.
Включение пула подключений
Включите пулинг, вызывая mssql_python.pooling() на уровне модуля до открытия любых соединений:
# database.py with connection pooling
import mssql_python
from flask import g, current_app
# Configure pool at module level
mssql_python.pooling(max_size=20, idle_timeout=300)
def get_db():
"""Get a database cursor with connection pooling."""
if "db_conn" not in g:
g.db_conn = mssql_python.connect(get_connection_string())
g.db_cursor = g.db_conn.cursor()
return g.db_cursor
Обработка ошибок
Flask позволяет регистрировать обработчики для определённых типов исключений. Перехват mssql_python.DatabaseError и mssql_python.IntegrityError позволяет возвращать структурированные JSON-ответы об ошибках вместо стандартных HTML-страниц ошибок.
Зарегистрировать обработчики ошибок
Добавьте эти обработчики в существующий app.py после строки app = Flask(__name__). Поскольку обработчики ссылаются на app объект, они должны появиться после создания приложения.
app.py требуется import mssql_python вверху. Обработчики возвращают структурированные JSON-ответы вместо стандартных страниц ошибок в HTML:
# app.py
import mssql_python
@app.errorhandler(mssql_python.DatabaseError)
def handle_database_error(error):
"""Handle database errors."""
return jsonify({"error": "Database error occurred"}), 500
@app.errorhandler(mssql_python.IntegrityError)
def handle_integrity_error(error):
"""Handle integrity constraint violations."""
error_msg = str(error)
if "UNIQUE" in error_msg:
return jsonify({"error": "Resource already exists"}), 409
if "FOREIGN KEY" in error_msg:
return jsonify({"error": "Referenced resource not found"}), 400
return jsonify({"error": "Data integrity error"}), 400
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Resource not found"}), 404
@app.errorhandler(400)
def bad_request(error):
return jsonify({"error": "Bad request"}), 400
Blueprints
По мере роста приложения поддерживать все маршруты в одном файле становится сложно. Flask Blueprints позволяет группировать связанные маршруты в отдельные модули, зарегистрированные в приложении.
Организуйте маршруты с помощью чертежей
Создайте модуль чертежа для маршрутов продукта, который импортирует get_db и определяет конечные точки под общим префиксом URL:
# blueprints/products.py
from flask import Blueprint, jsonify, request, abort
from database import get_db
products_bp = Blueprint("products", __name__, url_prefix="/api/products")
@products_bp.route("/")
def list_products():
"""List all products."""
cursor = get_db()
cursor.execute("""
SELECT ProductID, Name, ListPrice, Color, ProductCategoryID
FROM SalesLT.Product ORDER BY ProductID
""")
return jsonify([{
"id": row.ProductID,
"name": row.Name,
"price": float(row.ListPrice),
"color": row.Color,
"category_id": row.ProductCategoryID
} for row in cursor.fetchall()])
@products_bp.route("/<int:product_id>")
def get_product(product_id):
"""Get a product by ID."""
cursor = get_db()
cursor.execute(
"SELECT ProductID, Name, ListPrice, Color FROM SalesLT.Product WHERE ProductID = %(id)s",
{"id": product_id}
)
row = cursor.fetchone()
if not row:
abort(404)
return jsonify({"id": row.ProductID, "name": row.Name, "price": float(row.ListPrice), "color": row.Color})
Зарегистрируйте чертёж
Сохраните чертеж как blueprints/products.py, и добавьте пустой blueprints/__init__.py файл, чтобы Python рассматривал папку как пакет. Затем в app.py импортируйте план вместе с другими инструкциями импорта и зарегистрируйте его после строки app = Flask(__name__):
# app.py
from blueprints.products import products_bp
app.register_blueprint(products_bp)
Поскольку чертеж устанавливает url_prefix="/api/products", его маршруты обслуживаются под этим префиксом. Например, список маршрутов доступен по http://localhost:5000/api/products/адресу , отдельно от /products маршрутов, определённых непосредственно в app.py.
Testing
Flask предоставляет тестовый клиент, который отправляет запросы в ваше приложение без запуска настоящего HTTP-сервера. Используйте pytest фикстуры для создания клиента и повторного использования его в разных тестах.
Настройка тестов с pytest
Создайте фикстуру pytest, которая предоставляет тестовый клиент, и напишите тесты для проверки поведения маршрута:
# test_app.py
import uuid
import pytest
from app import app
@pytest.fixture
def client():
app.config["TESTING"] = True
with app.test_client() as client:
yield client
def test_health_check(client):
response = client.get("/health")
assert response.status_code == 200
data = response.get_json()
assert data["status"] == "healthy"
def test_list_products(client):
response = client.get("/products")
assert response.status_code == 200
data = response.get_json()
assert "items" in data
assert "total" in data
def test_create_product(client):
suffix = uuid.uuid4().hex[:8]
name = f"Test Product {suffix}"
response = client.post("/products", json={
"name": name,
"product_number": f"TEST-{suffix}",
"price": 19.99,
"category_id": 18
})
assert response.status_code == 201
data = response.get_json()
assert data["name"] == name
def test_get_product_not_found(client):
response = client.get("/products/99999")
assert response.status_code == 404
Эти тесты выполняются непосредственно в вашей рабочей базе данных, а не с моками, поэтому test_create_product вставляет реальную строку в SalesLT.Product. В AdventureWorksLT оба NameProductNumber имеют уникальные ограничения, поэтому тест генерирует уникальное значение для каждого при каждом запуске. Если вместо этого жёстко прописать эти значения, тест завершится с конфликтом при повторном запуске, если сначала не удалить эту строку.
Выполнение тестов
Сохраняйте тесты в test_app.py папке проекта. С активированной виртуальной средой установите pytest и запустите её из этой папки. Установка и запуск pytest в той же виртуальной среде и flaskmssql-python гарантирует, что тесты импортируют пакеты, которые использует ваше приложение.
pytest Автоматически test_app.py обнаруживает и сообщает о результатах:
pip install pytest
pytest
pytest автоматически обнаруживает test_app.py и сообщает о результатах:
==================== test session starts ====================
collected 4 items
test_app.py .... [100%]
===================== 4 passed in 3.21s =====================