Лекция 3. Работа с базами данных в веб-приложениях
1. Введение в базы данных
Что такое база данных
База данных (БД) — это организованная коллекция структурированных данных, хранящихся в электронном виде и управляемых системой управления базами данных (СУБД). В контексте веб-приложений БД служит для:
- Хранения данных пользователей, контента, настроек
- Обеспечения целостности данных через ограничения и транзакции
- Оптимизации доступа к данным через индексы и запросы
- Масштабирования хранения и обработки больших объёмов информации
Типы баз данных
Реляционные (SQL) БД
- Принцип: данные хранятся в таблицах со строгой схемой
- Связи: между таблицами через внешние ключи
- Язык запросов: SQL (Structured Query Language)
- Примеры: PostgreSQL, MySQL, SQLite, Oracle, SQL Server
Преимущества:
- ACID-транзакции (Atomicity, Consistency, Isolation, Durability)
- Строгая схема данных
- Мощные возможности запросов
- Зрелость и надёжность
Нереляционные (NoSQL) БД
- Документные: MongoDB, CouchDB
- Ключ-значение: Redis, DynamoDB
- Колоночные: Cassandra, HBase
- Графовые: Neo4j, ArangoDB
Преимущества:
- Гибкая схема данных
- Горизонтальное масштабирование
- Высокая производительность для специфических задач
2. SQL основы
Основные типы данных
| Тип | Описание | Примеры |
|---|---|---|
INTEGER | Целые числа | 1, -5, 1000 |
VARCHAR(n) | Строки переменной длины | 'Hello', 'World' |
TEXT | Длинные тексты | 'Описание товара...' |
BOOLEAN | Логические значения | TRUE, FALSE |
DECIMAL(p,s) | Числа с плавающей точкой | 99.99, 123.45 |
DATE | Даты | '2024-01-15' |
TIMESTAMP | Дата и время | '2024-01-15 14:30:00' |
UUID | Уникальные идентификаторы | '550e8400-e29b-41d4-a716-446655440000' |
Создание таблиц
-- Создание таблицы пользователейCREATE TABLE users ( id SERIAL PRIMARY KEY, email VARCHAR(255) UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
-- Создание таблицы верификации OTPCREATE TABLE otp_verification ( id SERIAL PRIMARY KEY, email VARCHAR(255) NOT NULL, code VARCHAR(6) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (email) REFERENCES users(email) ON DELETE CASCADE);Основные SQL операции
SELECT — выборка данных
-- Получить всех пользователейSELECT * FROM users;
-- Получить пользователя по emailSELECT * FROM users WHERE email = 'user@example.com';
-- Получить OTP коды для emailSELECT code, created_at FROM otp_verificationWHERE email = 'user@example.com'ORDER BY created_at DESC;
-- Подсчет пользователейSELECT COUNT(*) as total_users FROM users;
-- Получить последний OTP кодSELECT code FROM otp_verificationWHERE email = 'user@example.com'ORDER BY created_at DESCLIMIT 1;INSERT — вставка данных
-- Добавить пользователяINSERT INTO users (email)VALUES ('user@example.com');
-- Добавить OTP кодINSERT INTO otp_verification (email, code)VALUES ('user@example.com', '123456');
-- Добавить несколько OTP кодовINSERT INTO otp_verification (email, code) VALUES('user@example.com', '654321'),('admin@example.com', '789012');UPDATE — обновление данных
-- Обновить email пользователяUPDATE usersSET email = 'newemail@example.com'WHERE email = 'oldemail@example.com';
-- Обновить OTP кодUPDATE otp_verificationSET code = '999999'WHERE email = 'user@example.com'AND created_at = (SELECT MAX(created_at) FROM otp_verification WHERE email = 'user@example.com');DELETE — удаление данных
-- Удалить пользователя (OTP коды удалятся автоматически из-за CASCADE)DELETE FROM users WHERE email = 'user@example.com';
-- Удалить старые OTP коды (старше 10 минут)DELETE FROM otp_verificationWHERE created_at < NOW() - INTERVAL '10 minutes';Связи между таблицами
Внешние ключи (Foreign Keys)
-- Связь уже создана в CREATE TABLE-- otp_verification.email -> users.email
-- Добавить дополнительную связь по ID (если нужно)ALTER TABLE otp_verification ADD COLUMN user_id INTEGER;ALTER TABLE otp_verificationADD CONSTRAINT fk_otp_user_idFOREIGN KEY (user_id) REFERENCES users(id);JOIN операции
-- Получить пользователей с их последними OTP кодамиSELECT u.email, o.code, o.created_atFROM users uLEFT JOIN otp_verification o ON u.email = o.email;
-- Получить всех OTP коды с информацией о пользователяхSELECT u.email, o.code, o.created_atFROM otp_verification oJOIN users u ON o.email = u.emailORDER BY o.created_at DESC;3. Индексы и производительность
Что такое индексы
Индекс — это структура данных, которая ускоряет поиск и сортировку в таблице. Индексы создаются по одному или нескольким столбцам.
Типы индексов
Создание индексов
-- Индекс по email пользователей (уже уникальный)CREATE UNIQUE INDEX idx_users_email ON users(email);
-- Индекс по email в OTP таблицеCREATE INDEX idx_otp_email ON otp_verification(email);
-- Индекс по дате создания OTPCREATE INDEX idx_otp_created_at ON otp_verification(created_at);
-- Составной индекс для быстрого поиска OTP по email и датеCREATE INDEX idx_otp_email_created ON otp_verification(email, created_at);Анализ производительности запросов
-- Показать план выполнения запроса поиска OTPEXPLAIN SELECT * FROM otp_verification WHERE email = 'user@example.com';
-- Простой анализEXPLAIN ANALYZE SELECT * FROM otp_verificationWHERE email = 'user@example.com'ORDER BY created_at DESC LIMIT 1;4. Транзакции и ACID
Понятие транзакции
Транзакция — это последовательность операций с БД, которая выполняется как единое целое. Либо все операции выполняются успешно, либо ни одна не выполняется.
ACID свойства
- Atomicity (Атомарность) — все операции транзакции выполняются или не выполняются
- Consistency (Согласованность) — БД остается в корректном состоянии
- Isolation (Изолированность) — транзакции не влияют друг на друга
- Durability (Долговечность) — изменения сохраняются после завершения транзакции
Примеры транзакций
-- Начало транзакцииBEGIN;
-- Добавить пользователяINSERT INTO users (email) VALUES ('user@example.com');
-- Добавить OTP кодINSERT INTO otp_verification (email, code) VALUES ('user@example.com', '123456');
-- Подтвердить измененияCOMMIT;
-- В случае ошибки - откатить-- ROLLBACK;Уровни изоляции
-- Установка уровня изоляцииSET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Основные уровни:-- READ COMMITTED - читает только зафиксированные данные (по умолчанию)-- SERIALIZABLE - полная изоляция транзакций5. ORM и работа с БД из приложений
Что такое ORM
Object-Relational Mapping (ORM) — это технология программирования, которая связывает объекты в коде с таблицами в БД. ORM позволяет работать с БД используя объектно-ориентированный подход.
Популярные ORM
SQLAlchemy (Python)
from sqlalchemy import create_engine, Column, Integer, String, Booleanfrom sqlalchemy.ext.declarative import declarative_basefrom sqlalchemy.orm import sessionmaker
# Создание подключенияengine = create_engine('sqlite:///books.db')SessionLocal = sessionmaker(bind=engine)Base = declarative_base()
# Определение моделейclass User(Base): __tablename__ = "users"
id = Column(Integer, primary_key=True) email = Column(String(255), unique=True, nullable=False) created_at = Column(DateTime, default=datetime.utcnow)
class OTPVerification(Base): __tablename__ = "otp_verification"
id = Column(Integer, primary_key=True) email = Column(String(255), nullable=False) code = Column(String(6), nullable=False) created_at = Column(DateTime, default=datetime.utcnow)
# Работа с даннымиdef create_user(email: str): db = SessionLocal() user = User(email=email) db.add(user) db.commit() db.close() return user
def create_otp(email: str, code: str): db = SessionLocal() otp = OTPVerification(email=email, code=code) db.add(otp) db.commit() db.close() return otp
def get_latest_otp(email: str): db = SessionLocal() otp = db.query(OTPVerification).filter( OTPVerification.email == email ).order_by(OTPVerification.created_at.desc()).first() db.close() return otpPrisma (TypeScript/JavaScript)
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// Схема в schema.prisma// model User {// id Int @id @default(autoincrement())// email String @unique// createdAt DateTime @default(now())// otpCodes OTPVerification[]// }//// model OTPVerification {// id Int @id @default(autoincrement())// email String// code String// createdAt DateTime @default(now())// user User @relation(fields: [email], references: [email])// }
// Работа с даннымиasync function createUser(email: string) { return await prisma.user.create({ data: { email } })}
async function createOTP(email: string, code: string) { return await prisma.oTPVerification.create({ data: { email, code } })}
async function getLatestOTP(email: string) { return await prisma.oTPVerification.findFirst({ where: { email }, orderBy: { createdAt: 'desc' } })}Миграции схемы БД
Настройка Alembic
# Установка Alembicpip install alembic
# Инициализация в проектеalembic init alembic
# Настройка alembic.ini# sqlalchemy.url = sqlite:///./app.dbСоздание миграции
# Автоматическое создание миграцииalembic revision --autogenerate -m "Add users and otp_verification tables"
# Применение миграцииalembic upgrade head
# Откат миграцииalembic downgrade -1Пример миграции
"""Add users and otp_verification tables
Revision ID: 001Revises:Create Date: 2024-01-15 10:00:00.000000
"""from alembic import opimport sqlalchemy as sa
def upgrade(): # Создание таблицы users op.create_table('users', sa.Column('id', sa.Integer(), nullable=False), sa.Column('email', sa.String(255), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id'), sa.UniqueConstraint('email') )
# Создание таблицы otp_verification op.create_table('otp_verification', sa.Column('id', sa.Integer(), nullable=False), sa.Column('email', sa.String(255), nullable=False), sa.Column('code', sa.String(6), nullable=False), sa.Column('created_at', sa.DateTime(), nullable=True), sa.PrimaryKeyConstraint('id'), sa.ForeignKeyConstraint(['email'], ['users.email'], ondelete='CASCADE') )
# Создание индексов op.create_index('idx_otp_email', 'otp_verification', ['email']) op.create_index('idx_otp_created_at', 'otp_verification', ['created_at'])
def downgrade(): # Удаление индексов op.drop_index('idx_otp_created_at', table_name='otp_verification') op.drop_index('idx_otp_email', table_name='otp_verification')
# Удаление таблиц op.drop_table('otp_verification') op.drop_table('users')Управление миграциями
# Просмотр истории миграцийalembic history
# Просмотр текущей версииalembic current
# Применение до конкретной версииalembic upgrade 001
# Откат до конкретной версииalembic downgrade 000
# Создание пустой миграцииalembic revision -m "Custom migration"Пример добавления поля
"""Add status field to users table
Revision ID: 002Revises: 001Create Date: 2024-01-16 10:00:00.000000
"""from alembic import opimport sqlalchemy as sa
def upgrade(): # Добавление поля status в таблицу users op.add_column('users', sa.Column('status', sa.String(20), nullable=True, default='active'))
# Обновление существующих записей op.execute("UPDATE users SET status = 'active' WHERE status IS NULL")
# Создание индекса по статусу op.create_index('idx_users_status', 'users', ['status'])
def downgrade(): # Удаление индекса op.drop_index('idx_users_status', table_name='users')
# Удаление поля op.drop_column('users', 'status')Интеграция с FastAPI
# main.py - автоматическое применение миграций при запускеfrom alembic.config import Configfrom alembic import command
def run_migrations(): """Применить миграции при запуске приложения""" alembic_cfg = Config("alembic.ini") command.upgrade(alembic_cfg, "head")
# В FastAPI приложенииfrom contextlib import asynccontextmanager
@asynccontextmanagerasync def lifespan(app: FastAPI): # Применение миграций при запуске run_migrations() yield
app = FastAPI(lifespan=lifespan)6. Подключение к БД в веб-приложениях
Connection Pooling
from sqlalchemy import create_engine
# Настройка пула соединенийengine = create_engine( 'sqlite:///books.db', pool_size=5, # Количество соединений в пуле max_overflow=10 # Дополнительные соединения)Dependency Injection (FastAPI)
from fastapi import Dependsfrom sqlalchemy.orm import Session
def get_db(): db = SessionLocal() try: yield db finally: db.close()
@app.get("/users/")def get_users(db: Session = Depends(get_db)): users = db.query(User).all() return users
@app.post("/users/")def create_user(email: str, db: Session = Depends(get_db)): user = User(email=email) db.add(user) db.commit() return user
@app.post("/otp/")def create_otp(email: str, code: str, db: Session = Depends(get_db)): otp = OTPVerification(email=email, code=code) db.add(otp) db.commit() return otp
@app.get("/otp/{email}")def get_latest_otp(email: str, db: Session = Depends(get_db)): otp = db.query(OTPVerification).filter( OTPVerification.email == email ).order_by(OTPVerification.created_at.desc()).first() return otpОбработка ошибок БД
from fastapi import HTTPException
@app.post("/books/")def create_book(title: str, author: str, year: int, price: int, db: Session = Depends(get_db)): try: book = Book(title=title, author=author, year=year, price=price) db.add(book) db.commit() return book except Exception as e: db.rollback() raise HTTPException(status_code=500, detail="Database error")7. Безопасность работы с БД
SQL-инъекции
Проблема: Небезопасная конкатенация строк в SQL-запросах
# НЕПРАВИЛЬНО - уязвимо к SQL-инъекциямquery = f"SELECT * FROM users WHERE username = '{username}'"
# ПРАВИЛЬНО - использование параметризованных запросовquery = "SELECT * FROM users WHERE username = %s"cursor.execute(query, (username,))ORM защита
# SQLAlchemy автоматически защищает от SQL-инъекцийuser = db.query(User).filter(User.username == username).first()
# Использование bind параметровstmt = text("SELECT * FROM users WHERE username = :username")result = db.execute(stmt, {"username": username})Права доступа
-- Создание пользователя БДCREATE USER app_user WITH PASSWORD 'password';
-- Предоставление прав на таблицуGRANT SELECT, INSERT, UPDATE, DELETE ON books TO app_user;Шифрование данных
import hashlib
# Простое хеширование паролейdef hash_password(password: str) -> str: return hashlib.sha256(password.encode()).hexdigest()8. Оптимизация производительности
Анализ медленных запросов
-- Показать план выполненияEXPLAIN ANALYZE SELECT * FROM books WHERE author = 'Толстой';Кэширование
from functools import lru_cache
# Кэширование в памяти@lru_cache(maxsize=100)def get_book_by_id(book_id: int): return db.query(Book).filter(Book.id == book_id).first()9. Резервное копирование и восстановление
Создание бэкапов
# Бэкап SQLite базыcp books.db backup_$(date +%Y%m%d).db
# Бэкап PostgreSQLpg_dump -h localhost -U username -d books_db > backup.sqlВосстановление
# Восстановление SQLitecp backup_20240115.db books.db
# Восстановление PostgreSQLpsql -h localhost -U username -d books_db < backup.sql10. Мониторинг и диагностика
Мониторинг производительности
-- Размер таблицыSELECT pg_size_pretty(pg_total_relation_size('books'));
-- Количество записейSELECT COUNT(*) FROM books;Логирование
import logging
# Настройка логированияlogging.basicConfig(level=logging.INFO)logger = logging.getLogger(__name__)
# Логирование операцийdef create_book(title: str, author: str): logger.info(f"Creating book: {title} by {author}") # ... создание книги11. Современные подходы
Миграции схемы
# Alembic миграцияfrom alembic import opimport sqlalchemy as sa
def upgrade(): op.create_table('books', sa.Column('id', sa.Integer(), nullable=False), sa.Column('title', sa.String(255), nullable=False), sa.Column('author', sa.String(255), nullable=False), sa.Column('year', sa.Integer(), nullable=True), sa.Column('price', sa.Integer(), nullable=True), sa.PrimaryKeyConstraint('id') )
def downgrade(): op.drop_table('books')12. Практические рекомендации
Лучшие практики
- Всегда используйте параметризованные запросы для защиты от SQL-инъекций
- Создавайте индексы для часто используемых полей в WHERE и ORDER BY
- Используйте транзакции для операций, которые должны выполняться атомарно
- Настройте connection pooling для эффективного использования соединений
- Регулярно создавайте бэкапы и тестируйте процедуры восстановления
- Мониторьте производительность и оптимизируйте медленные запросы
- Используйте миграции для управления схемой БД
- Применяйте принцип наименьших привилегий для пользователей БД
Типичные ошибки
❌ Неправильно:
# SQL-инъекцияquery = f"SELECT * FROM users WHERE username = '{username}'"
# Отсутствие транзакцийdb.add(user)db.add(order)db.commit() # Если order не добавится, user останется✅ Правильно:
# Безопасный запросquery = "SELECT * FROM users WHERE username = %s"cursor.execute(query, (username,))
# Транзакцияwith db.begin(): db.add(user) db.add(order) # Автоматический commit или rollbackЗаключение
Работа с базами данных — критически важная часть разработки веб-приложений. Понимание:
- Основ SQL и реляционной модели данных
- Принципов ACID и управления транзакциями
- ORM технологий для удобной работы с БД
- Безопасности и защиты от атак
- Оптимизации производительности и мониторинга
- Резервного копирования и восстановления
позволяет создавать надёжные, безопасные и производительные веб-приложения, способные эффективно работать с большими объёмами данных.
Современные веб-приложения требуют не только знания основ, но и понимания современных подходов: микросервисной архитектуры, NoSQL решений, кэширования и горизонтального масштабирования.