Ескертпе
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Жүйеге кіруді немесе каталогтарды өзгертуді байқап көруге болады.
Бұл бетке кіру үшін қатынас шегін айқындау қажет. Каталогтарды өзгертуді байқап көруге болады.
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 =====================