├── example_10 ├── alembic │ ├── README │ ├── script.py.mako │ ├── env.py │ └── versions │ │ └── 53d52ce31539_added_initial_migration.py ├── README.md ├── models.py ├── alembic.ini └── blog_app.py ├── .gitignore ├── example_07 ├── README.md └── crud_operations.py ├── requirements.txt ├── example_09 └── README.md ├── example_01 ├── README.md └── connection.py ├── example_00_sql ├── script1.sql └── script2.sql ├── example_04 ├── README.md ├── one_to_many_v2.py └── one_to_many.py ├── example_02 ├── README.md └── fields.py ├── example_03 ├── README.md ├── one_to_one_v2.py └── one_to_one.py ├── example_05 ├── README.md ├── many_to_many_v2.py └── many_to_many.py ├── README.md └── example_08 ├── QUICKSTART.md ├── test_async.py ├── async_sqlite.py ├── README.md ├── SYNC_VS_ASYNC.md └── async_postgres.py /example_10/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | env/ 8 | venv/ 9 | *.egg-info/ 10 | dist/ 11 | build/ 12 | 13 | # Database files 14 | *.db 15 | *.sqlite 16 | *.sqlite3 17 | 18 | # Alembic 19 | alembic/versions/*.py 20 | !alembic/versions/.gitkeep 21 | 22 | # IDE 23 | .vscode/ 24 | .idea/ 25 | *.swp 26 | *.swo 27 | *~ 28 | 29 | # OS 30 | .DS_Store 31 | Thumbs.db 32 | -------------------------------------------------------------------------------- /example_07/README.md: -------------------------------------------------------------------------------- 1 | # Example 07: CRUD операции 2 | 3 | ## Описание 4 | 5 | Демонстрирует все основные операции с данными. 6 | 7 | ## Операции 8 | 9 | - **Create** - создание одной и нескольких записей 10 | - **Read** - различные способы чтения (all, first, get, filter) 11 | - **Update** - обновление через объект и массовое 12 | - **Delete** - удаление через объект и массовое 13 | 14 | ## Как запустить 15 | 16 | ```bash 17 | python crud_operations.py 18 | ``` 19 | 20 | ## Результат 21 | 22 | Демонстрирует все основные паттерны работы с данными в SQLAlchemy. 23 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Core dependencies 2 | SQLAlchemy>=2.0.0 3 | alembic>=1.12.0 4 | 5 | # Database drivers (synchronous) 6 | # PostgreSQL 7 | psycopg2-binary>=2.9.0 8 | # or 9 | # psycopg>=3.1.0 10 | 11 | # MySQL 12 | pymysql>=1.1.0 13 | # or 14 | # mysqlclient>=2.2.0 15 | 16 | # Async database drivers (for example_08) 17 | # SQLite async 18 | aiosqlite>=0.19.0 19 | 20 | # PostgreSQL async 21 | asyncpg>=0.29.0 22 | 23 | # MySQL async 24 | aiomysql>=0.2.0 25 | 26 | # Development 27 | ipython>=8.0.0 28 | pytest>=7.0.0 29 | pytest-asyncio>=0.21.0 # For async tests 30 | -------------------------------------------------------------------------------- /example_10/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | """Upgrade schema.""" 23 | ${upgrades if upgrades else "pass"} 24 | 25 | 26 | def downgrade() -> None: 27 | """Downgrade schema.""" 28 | ${downgrades if downgrades else "pass"} 29 | -------------------------------------------------------------------------------- /example_10/README.md: -------------------------------------------------------------------------------- 1 | # Example 10: Полное приложение - Блог 2 | 3 | ## Описание 4 | 5 | Полноценное приложение блога, использующее все изученные концепции. 6 | 7 | ## Функциональность 8 | 9 | - Пользователи с аутентификацией 10 | - Посты с публикацией 11 | - Комментарии 12 | - Теги (M2M связь) 13 | - Счетчик просмотров 14 | - Бизнес-логика в сервисах 15 | - Context manager для сессий 16 | 17 | ## Модели 18 | 19 | 1. **User** - пользователи 20 | 2. **Post** - посты 21 | 3. **Comment** - комментарии 22 | 4. **Tag** - теги 23 | 5. **post_tags** - связь M2M 24 | 25 | ## Структура 26 | 27 | - Модели с различными типами связей 28 | - Сервисный слой (BlogService) 29 | - Безопасная работа с сессиями 30 | - Статистика и отчеты 31 | 32 | ## Как запустить 33 | 34 | ```bash 35 | python blog_app.py 36 | ``` 37 | 38 | ## Результат 39 | 40 | - База данных `blog.db` 41 | - Тестовые данные 42 | - Статистика блога 43 | - Список постов с деталями 44 | -------------------------------------------------------------------------------- /example_09/README.md: -------------------------------------------------------------------------------- 1 | # Example 09: Миграции с Alembic 2 | 3 | ## Описание 4 | 5 | Этот пример демонстрирует работу с миграциями баз данных с помощью Alembic. 6 | 7 | ## Установка 8 | 9 | ```bash 10 | pip install alembic 11 | ``` 12 | 13 | ## Инициализация Alembic 14 | 15 | ```bash 16 | cd example_09 17 | alembic init alembic 18 | ``` 19 | 20 | ## Основные команды 21 | 22 | ```bash 23 | # Создание миграции 24 | alembic revision -m "create users table" 25 | 26 | # Автоматическая генерация миграции 27 | alembic revision --autogenerate -m "add email column" 28 | 29 | # Применение миграций 30 | alembic upgrade head 31 | 32 | # Откат на одну версию назад 33 | alembic downgrade -1 34 | 35 | # Откат всех миграций 36 | alembic downgrade base 37 | 38 | # История миграций 39 | alembic history 40 | 41 | # Текущая версия 42 | alembic current 43 | ``` 44 | 45 | ## Структура 46 | 47 | - `alembic/` - директория с миграциями 48 | - `alembic.ini` - конфигурация Alembic 49 | - `models.py` - модели SQLAlchemy 50 | - `database.py` - настройка подключения к БД 51 | 52 | ## Как это работает 53 | 54 | 1. Alembic отслеживает версии схемы БД 55 | 2. Каждая миграция - это Python скрипт с функциями upgrade() и downgrade() 56 | 3. Alembic создает таблицу alembic_version для отслеживания текущей версии 57 | 4. Можно автогенерировать миграции на основе изменений моделей 58 | 59 | ## Пример миграции 60 | 61 | ```python 62 | def upgrade(): 63 | op.create_table( 64 | 'users', 65 | sa.Column('id', sa.Integer(), primary_key=True), 66 | sa.Column('username', sa.String(50), nullable=False), 67 | sa.Column('email', sa.String(100)) 68 | ) 69 | 70 | def downgrade(): 71 | op.drop_table('users') 72 | ``` 73 | 74 | -------------------------------------------------------------------------------- /example_01/README.md: -------------------------------------------------------------------------------- 1 | # Example 01: Подключение к базе данных 2 | 3 | ## Описание 4 | 5 | Этот пример демонстрирует различные способы подключения к базам данных с помощью SQLAlchemy. 6 | 7 | ## Что включено 8 | 9 | 1. **SQLite подключение** - работа с локальной файловой БД 10 | 2. **PostgreSQL подключение** - подключение к PostgreSQL серверу 11 | 3. **MySQL подключение** - подключение к MySQL серверу 12 | 4. **Session Management** - различные способы работы с сессиями 13 | 5. **Connection Pooling** - настройка пула соединений 14 | 6. **Database URL** - примеры URL для разных БД 15 | 16 | ## Как запустить 17 | 18 | ```bash 19 | # Установите зависимости 20 | pip install sqlalchemy 21 | 22 | # Для PostgreSQL 23 | pip install psycopg2-binary 24 | 25 | # Для MySQL 26 | pip install pymysql 27 | 28 | # Запустите пример 29 | python connection.py 30 | ``` 31 | 32 | ## Ключевые концепции 33 | 34 | ### Engine 35 | 36 | Engine - это точка входа для работы с БД. Он управляет соединениями и их пулом. 37 | 38 | ```python 39 | engine = create_engine('sqlite:///example.db') 40 | ``` 41 | 42 | ### Session 43 | 44 | Session - это интерфейс для выполнения операций с БД (ORM). 45 | 46 | ```python 47 | Session = sessionmaker(bind=engine) 48 | session = Session() 49 | ``` 50 | 51 | ### Connection Pool 52 | 53 | Пул соединений позволяет переиспользовать соединения вместо создания новых для каждого запроса. 54 | 55 | ```python 56 | engine = create_engine( 57 | 'postgresql://user:pass@localhost/db', 58 | pool_size=5, 59 | max_overflow=10 60 | ) 61 | ``` 62 | 63 | ## Результат 64 | 65 | После запуска вы увидите: 66 | - Успешные подключения к SQLite 67 | - Примеры работы с сессиями 68 | - Настройки пула соединений 69 | - Список Database URL для разных СУБД 70 | -------------------------------------------------------------------------------- /example_00_sql/script1.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE users ( 2 | id INTEGER PRIMARY KEY AUTOINCREMENT, 3 | first_name VARCHAR(30), 4 | last_name VARCHAR(30), 5 | email VARCHAR(30) NOT NULL 6 | ); 7 | 8 | INSERT INTO users 9 | (first_name, last_name, email) 10 | VALUES 11 | ('Pavel', 'Akhtyamov', 'akhtyamovpavel@gmail.com'), 12 | ('Yuriy', 'Petrov', 'petrov@gmail.com') 13 | ; 14 | 15 | INSERT INTO users 16 | (first_name, last_name, email) 17 | VALUES 18 | ('Hoba', 'Petrov', 'petrov2@gmail.com'); 19 | 20 | 21 | SELECT first_name, last_name, email 22 | FROM users; 23 | 24 | SELECT id, first_name, last_name, email 25 | FROM users; 26 | 27 | -- выбрать все поля из базы 28 | SELECT * 29 | FROM users; 30 | 31 | -- выбрать первые две записи 32 | SELECT * 33 | FROM users LIMIT 2; 34 | 35 | SELECT * 36 | FROM users 37 | ORDER BY first_name 38 | LIMIT 2; 39 | 40 | -- По умолчанию сортировка поля - ASC 41 | SELECT * 42 | FROM users 43 | ORDER BY last_name, first_name DESC 44 | LIMIT 2; 45 | 46 | SELECT * 47 | FROM users 48 | ORDER BY last_name, first_name ASC 49 | LIMIT 2; 50 | 51 | 52 | SELECT * 53 | FROM users 54 | WHERE first_name = 'Hoba' AND last_name = 'Petrov'; 55 | 56 | -- Исправим пользователя Hoba Petrov 57 | 58 | UPDATE users 59 | SET id = 3 60 | WHERE first_name = 'Hoba' AND last_name = 'Petrov'; 61 | 62 | 63 | SELECT * 64 | FROM users 65 | WHERE first_name = 'Hoba' AND last_name = 'Petrov'; 66 | 67 | ALTER TABLE 68 | users 69 | ADD date_birth DATE; 70 | 71 | SELECT * FROM users; 72 | 73 | -- Удаляем данные 74 | DELETE FROM users 75 | WHERE first_name = 'Hoba'; 76 | 77 | SELECT * FROM users; 78 | 79 | -- удалить таблицу из схемы 80 | DROP TABLE users; 81 | 82 | -- Создаем таблицу автомобилей 83 | 84 | CREATE TABLE autos 85 | ( 86 | id INTEGER PRIMARY KEY AUTOINCREMENT, 87 | user_id INTEGER NOT NULL, 88 | name VARCHAR(30), 89 | FOREIGN KEY (user_id) REFERENCES users(id) 90 | ); 91 | 92 | SELECT * FROM users; 93 | 94 | -- SQLITE не работает Foreign Key! 95 | INSERT INTO autos (user_id, name) 96 | VALUES (4, 'Kamaz'); 97 | 98 | 99 | -------------------------------------------------------------------------------- /example_04/README.md: -------------------------------------------------------------------------------- 1 | # Example 04: Связь One-to-Many (1-Many) 2 | 3 | ## Описание 4 | 5 | Демонстрирует связь один-ко-многим - самый распространенный тип связи. 6 | 7 | ## ER-диаграмма 8 | 9 | ```mermaid 10 | erDiagram 11 | BLOG_USER ||--o{ BLOG_POST : "writes" 12 | BLOG_USER ||--o{ COMMENT : "writes" 13 | BLOG_POST ||--o{ COMMENT : "has" 14 | 15 | BLOG_USER { 16 | int id PK 17 | string username UK 18 | string email UK 19 | } 20 | 21 | BLOG_POST { 22 | int id PK 23 | string title 24 | text content 25 | datetime created_at 26 | boolean is_published 27 | int author_id FK "references blog_users(id)" 28 | } 29 | 30 | COMMENT { 31 | int id PK 32 | text content 33 | datetime created_at 34 | int author_id FK "references blog_users(id)" 35 | int post_id FK "references blog_posts(id)" 36 | } 37 | ``` 38 | 39 | **Пояснение:** 40 | - `||--o{` - связь один-ко-многим (один User может иметь много Posts) 41 | - `PK` - Primary Key 42 | - `FK` - Foreign Key 43 | - `UK` - Unique Key 44 | - Один пользователь (`BLOG_USER`) может написать много постов (`BLOG_POST`) 45 | - Один пользователь может оставить много комментариев (`COMMENT`) 46 | - Один пост может иметь много комментариев 47 | 48 | ## Примеры 49 | 50 | 1. **Author и Books** - один автор, много книг 51 | 2. **Blog** - пользователи, посты, комментарии (вложенные связи) 52 | 3. **Cascade Delete** - автоматическое удаление зависимых объектов 53 | 54 | ## Как запустить 55 | 56 | ```bash 57 | python one_to_many.py 58 | ``` 59 | 60 | ## Ключевые концепции 61 | 62 | ### ForeignKey без unique 63 | 64 | ```python 65 | author_id = Column(Integer, ForeignKey('authors.id')) 66 | ``` 67 | 68 | ### relationship() 69 | 70 | ```python 71 | # На стороне "одного" 72 | books = relationship("Book", back_populates="author") 73 | 74 | # На стороне "многих" 75 | author = relationship("Author", back_populates="books") 76 | ``` 77 | 78 | ### Cascade операции 79 | 80 | При удалении автора удаляются все его книги. 81 | 82 | ## Результат 83 | 84 | - База данных `one_to_many.db` 85 | - Демонстрация создания, чтения и каскадного удаления 86 | -------------------------------------------------------------------------------- /example_02/README.md: -------------------------------------------------------------------------------- 1 | # Example 02: Основные типы полей 2 | 3 | ## Описание 4 | 5 | Этот пример демонстрирует все основные типы полей в SQLAlchemy и их использование. 6 | 7 | ## Что включено 8 | 9 | 1. **Числовые типы** - Integer, BigInteger, SmallInteger, Float, Numeric 10 | 2. **Строковые типы** - String, Text 11 | 3. **Дата и время** - Date, DateTime, Time, Interval 12 | 4. **Булевы значения** - Boolean 13 | 5. **JSON** - хранение сложных структур данных 14 | 6. **Enum** - перечисления для ограниченного набора значений 15 | 7. **Бинарные данные** - LargeBinary для файлов 16 | 8. **Ограничения** - NOT NULL, UNIQUE, CHECK, DEFAULT, INDEX 17 | 18 | ## Как запустить 19 | 20 | ```bash 21 | python fields.py 22 | ``` 23 | 24 | ## Модели 25 | 26 | ### Product 27 | Демонстрирует числовые типы различной точности. 28 | 29 | ### Article 30 | Показывает работу со строковыми типами разной длины. 31 | 32 | ### Event 33 | Примеры работы с датой и временем. 34 | 35 | ### User 36 | Булевы флаги для пользователей. 37 | 38 | ### Settings 39 | JSON для хранения настроек. 40 | 41 | ### Order / UserProfile 42 | Enum для ограниченного набора значений (статусы, роли). 43 | 44 | ### File 45 | Хранение бинарных данных. 46 | 47 | ### Employee 48 | Полный пример с различными ограничениями и индексами. 49 | 50 | ## Ключевые концепции 51 | 52 | ### Числовые типы 53 | - `Integer` - стандартный 32-bit integer 54 | - `BigInteger` - 64-bit для больших чисел 55 | - `Numeric(precision, scale)` - точные числа с фиксированной точностью 56 | 57 | ### Строки 58 | - `String(length)` - VARCHAR с ограничением 59 | - `Text` - неограниченный текст 60 | 61 | ### Ограничения 62 | - `nullable=False` - NOT NULL 63 | - `unique=True` - UNIQUE constraint 64 | - `CheckConstraint('expression')` - CHECK constraint 65 | - `default=value` - значение по умолчанию 66 | - `index=True` - создание индекса 67 | 68 | ### JSON 69 | Можно хранить любые сериализуемые в JSON структуры (словари, списки). 70 | 71 | ### Enum 72 | Используйте `enum.Enum` для полей с ограниченным набором значений. 73 | 74 | ## Результат 75 | 76 | После запуска: 77 | - Создается база данных `fields_example.db` 78 | - Создаются таблицы для всех моделей 79 | - Заполняются примеры данных 80 | - Выводится информация о созданных записях 81 | -------------------------------------------------------------------------------- /example_10/alembic/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | if config.config_file_name is not None: 15 | fileConfig(config.config_file_name) 16 | 17 | # add your model's MetaData object here 18 | # for 'autogenerate' support 19 | # from myapp import mymodel 20 | # target_metadata = mymodel.Base.metadata 21 | from models import Base 22 | target_metadata = Base.metadata 23 | 24 | # other values from the config, defined by the needs of env.py, 25 | # can be acquired: 26 | # my_important_option = config.get_main_option("my_important_option") 27 | # ... etc. 28 | 29 | 30 | def run_migrations_offline() -> None: 31 | """Run migrations in 'offline' mode. 32 | 33 | This configures the context with just a URL 34 | and not an Engine, though an Engine is acceptable 35 | here as well. By skipping the Engine creation 36 | we don't even need a DBAPI to be available. 37 | 38 | Calls to context.execute() here emit the given string to the 39 | script output. 40 | 41 | """ 42 | url = config.get_main_option("sqlalchemy.url") 43 | context.configure( 44 | url=url, 45 | target_metadata=target_metadata, 46 | literal_binds=True, 47 | dialect_opts={"paramstyle": "named"}, 48 | ) 49 | 50 | with context.begin_transaction(): 51 | context.run_migrations() 52 | 53 | 54 | def run_migrations_online() -> None: 55 | """Run migrations in 'online' mode. 56 | 57 | In this scenario we need to create an Engine 58 | and associate a connection with the context. 59 | 60 | """ 61 | connectable = engine_from_config( 62 | config.get_section(config.config_ini_section, {}), 63 | prefix="sqlalchemy.", 64 | poolclass=pool.NullPool, 65 | ) 66 | 67 | with connectable.connect() as connection: 68 | context.configure( 69 | connection=connection, target_metadata=target_metadata 70 | ) 71 | 72 | with context.begin_transaction(): 73 | context.run_migrations() 74 | 75 | 76 | if context.is_offline_mode(): 77 | run_migrations_offline() 78 | else: 79 | run_migrations_online() 80 | -------------------------------------------------------------------------------- /example_03/README.md: -------------------------------------------------------------------------------- 1 | # Example 03: Связь One-to-One (1-1) 2 | 3 | ## Описание 4 | 5 | Этот пример демонстрирует реализацию связи один-к-одному между таблицами в SQLAlchemy. 6 | 7 | ## ER-диаграмма 8 | 9 | ```mermaid 10 | erDiagram 11 | USER ||--o| USER_PROFILE : "has one" 12 | 13 | USER { 14 | int id PK 15 | string username UK 16 | string email UK 17 | } 18 | 19 | USER_PROFILE { 20 | int id PK 21 | int user_id FK,UK "references users(id)" 22 | string first_name 23 | string last_name 24 | string bio 25 | string avatar_url 26 | date birth_date 27 | } 28 | ``` 29 | 30 | **Пояснение:** 31 | - `||--o|` - связь один-к-одному (один User может иметь максимум один UserProfile) 32 | - `PK` - Primary Key (первичный ключ) 33 | - `FK` - Foreign Key (внешний ключ) 34 | - `UK` - Unique Key (уникальное ограничение) 35 | - `user_id` имеет `UNIQUE` constraint, что обеспечивает связь 1-1 36 | 37 | ## Что включено 38 | 39 | 1. **User и UserProfile** - пользователь и его профиль 40 | 2. **Employee и Workstation** - сотрудник и рабочее место 41 | 3. **Person и Passport** - человек и его паспорт 42 | 4. **Cascade Delete** - каскадное удаление связанных объектов 43 | 5. **Querying** - различные способы запросов 44 | 45 | ## Как запустить 46 | 47 | ```bash 48 | python one_to_one.py 49 | ``` 50 | 51 | ## Ключевые концепции 52 | 53 | ### uselist=False 54 | 55 | Делает связь one-to-one вместо one-to-many: 56 | 57 | ```python 58 | profile = relationship("UserProfile", back_populates="user", uselist=False) 59 | ``` 60 | 61 | ### unique=True на ForeignKey 62 | 63 | Обеспечивает уникальность связи на уровне БД: 64 | 65 | ```python 66 | user_id = Column(Integer, ForeignKey('users.id'), unique=True) 67 | ``` 68 | 69 | ### Cascade операции 70 | 71 | Автоматическое удаление связанных объектов: 72 | 73 | ```python 74 | profile = relationship( 75 | "UserProfile", 76 | cascade="all, delete-orphan", 77 | uselist=False 78 | ) 79 | ``` 80 | 81 | ### Создание связи 82 | 83 | Два способа: 84 | 85 | ```python 86 | # Способ 1: Явное присваивание 87 | user = User(username="john") 88 | profile = UserProfile(bio="Developer") 89 | user.profile = profile 90 | 91 | # Способ 2: Вложенное создание 92 | user = User( 93 | username="john", 94 | profile=UserProfile(bio="Developer") 95 | ) 96 | ``` 97 | 98 | ## Результат 99 | 100 | После запуска: 101 | - Создается база данных `one_to_one.db` 102 | - Демонстрируются 5 различных сценариев 103 | - Показывается каскадное удаление 104 | - Примеры различных запросов 105 | -------------------------------------------------------------------------------- /example_05/README.md: -------------------------------------------------------------------------------- 1 | # Example 05: Связь Many-to-Many 2 | 3 | ## Описание 4 | 5 | Демонстрирует связь многие-ко-многим через ассоциативную таблицу. 6 | 7 | ## ER-диаграммы 8 | 9 | ### Вариант 1: Простая ассоциативная таблица (Post-Tag) 10 | 11 | ```mermaid 12 | erDiagram 13 | POST }o--o{ TAG : "tagged with" 14 | 15 | POST { 16 | int id PK 17 | string title 18 | text content 19 | } 20 | 21 | POST_TAGS { 22 | int post_id PK,FK "references posts(id)" 23 | int tag_id PK,FK "references tags(id)" 24 | } 25 | 26 | TAG { 27 | int id PK 28 | string name UK 29 | } 30 | ``` 31 | 32 | **Пояснение:** 33 | - `}o--o{` - связь многие-ко-многим 34 | - `POST_TAGS` - ассоциативная таблица (junction table) 35 | - Один пост может иметь много тегов 36 | - Один тег может быть у многих постов 37 | 38 | ### Вариант 2: Ассоциативная таблица с данными (Student-Course) 39 | 40 | ```mermaid 41 | erDiagram 42 | STUDENT }o--o{ COURSE : "enrolled in" 43 | 44 | STUDENT { 45 | int id PK 46 | string name 47 | string email 48 | } 49 | 50 | STUDENT_COURSES { 51 | int student_id PK,FK "references students(id)" 52 | int course_id PK,FK "references courses(id)" 53 | datetime enrolled_at 54 | int grade 55 | boolean completed 56 | } 57 | 58 | COURSE { 59 | int id PK 60 | string name 61 | int credits 62 | } 63 | ``` 64 | 65 | **Пояснение:** 66 | - `STUDENT_COURSES` - ассоциативная таблица с дополнительными полями 67 | - Хранит не только связь, но и данные о записи (оценка, дата, статус) 68 | - Используется как полноценная модель в SQLAlchemy 69 | 70 | ## Примеры 71 | 72 | 1. **Posts и Tags** - простая связь M2M 73 | 2. **Students и Courses** - M2M с дополнительными данными (оценки, даты) 74 | 75 | ## Как запустить 76 | 77 | ```bash 78 | python many_to_many.py 79 | ``` 80 | 81 | ## Ключевые концепции 82 | 83 | ### Простая ассоциативная таблица 84 | 85 | ```python 86 | post_tags = Table( 87 | 'post_tags', 88 | Base.metadata, 89 | Column('post_id', Integer, ForeignKey('posts.id')), 90 | Column('tag_id', Integer, ForeignKey('tags.id')) 91 | ) 92 | ``` 93 | 94 | ### Ассоциативная таблица как модель 95 | 96 | Когда нужны дополнительные данные о связи: 97 | 98 | ```python 99 | class StudentCourse(Base): 100 | student_id = Column(Integer, ForeignKey('students.id'), primary_key=True) 101 | course_id = Column(Integer, ForeignKey('courses.id'), primary_key=True) 102 | grade = Column(Integer) 103 | enrolled_at = Column(DateTime) 104 | ``` 105 | 106 | ## Результат 107 | 108 | - База данных `many_to_many.db` 109 | - Примеры обеих подходов к M2M 110 | -------------------------------------------------------------------------------- /example_00_sql/script2.sql: -------------------------------------------------------------------------------- 1 | -- Postgres 2 | CREATE TABLE users ( 3 | id SERIAL primary KEY, 4 | first_name VARCHAR(30), 5 | last_name VARCHAR(30), 6 | email VARCHAR(30) NOT NULL 7 | ); 8 | 9 | INSERT INTO users 10 | (first_name, last_name, email) 11 | VALUES 12 | ('Pavel', 'Akhtyamov', 'akhtyamovpavel@gmail.com'), 13 | ('Yuriy', 'Petrov', 'petrov@gmail.com') 14 | ; 15 | 16 | INSERT INTO users 17 | (first_name, last_name, email) 18 | VALUES 19 | ('Hoba', 'Petrov', 'petrov2@gmail.com'); 20 | 21 | 22 | SELECT first_name, last_name, email 23 | FROM users; 24 | 25 | SELECT id, first_name, last_name, email 26 | FROM users; 27 | 28 | -- выбрать все поля из базы 29 | SELECT * 30 | FROM users; 31 | 32 | -- выбрать первые две записи 33 | SELECT * 34 | FROM users LIMIT 2; 35 | 36 | SELECT * 37 | FROM users 38 | ORDER BY first_name 39 | LIMIT 2; 40 | 41 | -- По умолчанию сортировка поля - ASC 42 | SELECT * 43 | FROM users 44 | ORDER BY last_name, first_name DESC 45 | LIMIT 2; 46 | 47 | SELECT * 48 | FROM users 49 | ORDER BY last_name, first_name ASC 50 | LIMIT 2; 51 | 52 | 53 | SELECT * 54 | FROM users 55 | WHERE first_name = 'Hoba' AND last_name = 'Petrov'; 56 | 57 | -- Исправим пользователя Hoba Petrov 58 | 59 | UPDATE users 60 | SET id = 3 61 | WHERE first_name = 'Hoba' AND last_name = 'Petrov'; 62 | 63 | 64 | SELECT * 65 | FROM users 66 | WHERE first_name = 'Hoba' AND last_name = 'Petrov'; 67 | 68 | ALTER TABLE 69 | users 70 | ADD date_birth DATE; 71 | 72 | SELECT * FROM users; 73 | 74 | -- Удаляем данные 75 | DELETE FROM users 76 | WHERE first_name = 'Hoba'; 77 | 78 | SELECT * FROM users; 79 | 80 | -- удалить таблицу из схемы 81 | DROP TABLE users; 82 | 83 | -- Создаем таблицу автомобилей 84 | 85 | CREATE TABLE autos 86 | ( 87 | id SERIAL PRIMARY KEY, 88 | user_id INTEGER NOT NULL, 89 | name VARCHAR(30), 90 | FOREIGN KEY (user_id) REFERENCES users(id) 91 | ); 92 | 93 | SELECT * FROM users; 94 | 95 | -- SQLITE не работает Foreign Key! 96 | -- В Postgres - работает! 97 | INSERT INTO autos (user_id, name) 98 | VALUES (3, 'Kamaz'); 99 | 100 | insert into autos (user_id, name) 101 | values (2, 'BMW'), (2, 'Kamaz'), (1, 'Audi'); 102 | 103 | select * from autos; 104 | 105 | -- У кого есть kamaz-ы? 106 | 107 | select users.id, users.first_name, users.last_name, autos.name 108 | from users inner join autos on users.id = autos.user_id ; 109 | 110 | -- А у кого в итоге Камазы? 111 | select users.id, users.first_name, users.last_name, autos.name 112 | from users inner join autos on users.id = autos.user_id 113 | where autos.name = 'Kamaz'; 114 | 115 | -- У кого какое количество машин? 116 | select a.name, count(a.id) as cnt 117 | from autos a 118 | group by a.name 119 | order by cnt DESC; 120 | 121 | select a.user_id, COUNT(a.id) 122 | from autos a 123 | group by a.user_id; 124 | 125 | -------------------------------------------------------------------------------- /example_10/alembic/versions/53d52ce31539_added_initial_migration.py: -------------------------------------------------------------------------------- 1 | """Added initial migration 2 | 3 | Revision ID: 53d52ce31539 4 | Revises: 5 | Create Date: 2025-11-22 16:55:04.371864 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '53d52ce31539' 16 | down_revision: Union[str, Sequence[str], None] = None 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | """Upgrade schema.""" 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | op.create_table('tags', 25 | sa.Column('id', sa.Integer(), nullable=False), 26 | sa.Column('name', sa.String(length=50), nullable=False), 27 | sa.PrimaryKeyConstraint('id'), 28 | sa.UniqueConstraint('name') 29 | ) 30 | op.create_table('users', 31 | sa.Column('id', sa.Integer(), nullable=False), 32 | sa.Column('username', sa.String(length=50), nullable=False), 33 | sa.Column('email', sa.String(length=100), nullable=False), 34 | sa.Column('password_hash', sa.String(length=255), nullable=False), 35 | sa.Column('is_active', sa.Boolean(), nullable=False), 36 | sa.Column('created_at', sa.DateTime(), nullable=False), 37 | sa.PrimaryKeyConstraint('id'), 38 | sa.UniqueConstraint('email'), 39 | sa.UniqueConstraint('username') 40 | ) 41 | op.create_table('posts', 42 | sa.Column('id', sa.Integer(), nullable=False), 43 | sa.Column('title', sa.String(length=200), nullable=False), 44 | sa.Column('slug', sa.String(length=200), nullable=False), 45 | sa.Column('content', sa.String(), nullable=False), 46 | sa.Column('is_published', sa.Boolean(), nullable=False), 47 | sa.Column('view_count', sa.Integer(), nullable=False), 48 | sa.Column('created_at', sa.DateTime(), nullable=False), 49 | sa.Column('updated_at', sa.DateTime(), nullable=False), 50 | sa.Column('author_id', sa.Integer(), nullable=False), 51 | sa.ForeignKeyConstraint(['author_id'], ['users.id'], ), 52 | sa.PrimaryKeyConstraint('id'), 53 | sa.UniqueConstraint('slug') 54 | ) 55 | op.create_table('comments', 56 | sa.Column('id', sa.Integer(), nullable=False), 57 | sa.Column('content', sa.String(), nullable=False), 58 | sa.Column('created_at', sa.DateTime(), nullable=False), 59 | sa.Column('author_id', sa.Integer(), nullable=False), 60 | sa.Column('post_id', sa.Integer(), nullable=False), 61 | sa.ForeignKeyConstraint(['author_id'], ['users.id'], ), 62 | sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ), 63 | sa.PrimaryKeyConstraint('id') 64 | ) 65 | op.create_table('post_tags', 66 | sa.Column('post_id', sa.Integer(), nullable=False), 67 | sa.Column('tag_id', sa.Integer(), nullable=False), 68 | sa.ForeignKeyConstraint(['post_id'], ['posts.id'], ), 69 | sa.ForeignKeyConstraint(['tag_id'], ['tags.id'], ), 70 | sa.PrimaryKeyConstraint('post_id', 'tag_id') 71 | ) 72 | # ### end Alembic commands ### 73 | 74 | 75 | def downgrade() -> None: 76 | """Downgrade schema.""" 77 | # ### commands auto generated by Alembic - please adjust! ### 78 | op.drop_table('post_tags') 79 | op.drop_table('comments') 80 | op.drop_table('posts') 81 | op.drop_table('users') 82 | op.drop_table('tags') 83 | # ### end Alembic commands ### 84 | -------------------------------------------------------------------------------- /example_10/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from datetime import timezone 3 | from typing import List 4 | from sqlalchemy.orm import Mapped, declarative_base, mapped_column, relationship 5 | from sqlalchemy import Column, ForeignKey, Integer, String, Table, create_engine 6 | 7 | Base = declarative_base() 8 | engine = create_engine('sqlite:///blog.db', echo=False) 9 | 10 | 11 | # ============================================ 12 | # Модели 13 | # ============================================ 14 | 15 | # Many-to-Many: Posts <-> Tags 16 | post_tags = Table( 17 | 'post_tags', 18 | Base.metadata, 19 | Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True), 20 | Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True) 21 | ) 22 | 23 | 24 | class User(Base): 25 | """Пользователь""" 26 | __tablename__ = 'users' 27 | 28 | id: Mapped[int] = mapped_column(primary_key=True) 29 | username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) 30 | email: Mapped[str] = mapped_column(String(100), unique=True, nullable=False) 31 | password_hash: Mapped[str] = mapped_column(String(255), nullable=False) 32 | is_active: Mapped[bool] = mapped_column(default=True) 33 | created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc)) 34 | 35 | # Relationships 36 | posts: Mapped[List["Post"]] = relationship(back_populates="author", cascade="all, delete-orphan") 37 | comments: Mapped[List["Comment"]] = relationship(back_populates="author", cascade="all, delete-orphan") 38 | 39 | def __repr__(self) -> str: 40 | return f"" 41 | 42 | class Post(Base): 43 | """Пост""" 44 | __tablename__ = 'posts' 45 | 46 | id: Mapped[int] = mapped_column(primary_key=True) 47 | title: Mapped[str] = mapped_column(String(200), nullable=False) 48 | slug: Mapped[str] = mapped_column(String(200), unique=True, nullable=False) 49 | content: Mapped[str | None] = mapped_column(nullable=False) 50 | is_published: Mapped[bool] = mapped_column(default=False) 51 | view_count: Mapped[int] = mapped_column(default=0) 52 | created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc)) 53 | updated_at: Mapped[datetime] = mapped_column( 54 | default=lambda: datetime.now(timezone.utc), 55 | onupdate=lambda: datetime.now(timezone.utc), 56 | ) 57 | 58 | author_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False) 59 | 60 | # Relationships 61 | author: Mapped["User"] = relationship(back_populates="posts") 62 | comments: Mapped[List["Comment"]] = relationship(back_populates="post", cascade="all, delete-orphan") 63 | tags: Mapped[List["Tag"]] = relationship(secondary=post_tags, back_populates="posts") 64 | 65 | def __repr__(self) -> str: 66 | return f"" 67 | 68 | 69 | class Comment(Base): 70 | """Комментарий""" 71 | __tablename__ = 'comments' 72 | 73 | id: Mapped[int] = mapped_column(primary_key=True) 74 | content: Mapped[str | None] = mapped_column(nullable=False) 75 | created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc)) 76 | 77 | author_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False) 78 | post_id: Mapped[int] = mapped_column(ForeignKey('posts.id'), nullable=False) 79 | 80 | # Relationships 81 | author: Mapped["User"] = relationship(back_populates="comments") 82 | post: Mapped["Post"] = relationship(back_populates="comments") 83 | 84 | def __repr__(self) -> str: 85 | return f"" 86 | 87 | 88 | class Tag(Base): 89 | """Тег""" 90 | __tablename__ = 'tags' 91 | 92 | id: Mapped[int] = mapped_column(primary_key=True) 93 | name: Mapped[str] = mapped_column(String(50), unique=True, nullable=False) 94 | 95 | posts: Mapped[List["Post"]] = relationship(secondary=post_tags, back_populates="tags") 96 | 97 | def __repr__(self) -> str: 98 | return f"" 99 | -------------------------------------------------------------------------------- /example_03/one_to_one_v2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 03: Связь One-to-One (SQLAlchemy 2.0) 3 | Демонстрирует реализацию связи один-к-одному с современным синтаксисом 4 | """ 5 | 6 | from sqlalchemy import create_engine, String, ForeignKey, select 7 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session, selectinload 8 | from datetime import date 9 | from typing import Optional 10 | 11 | 12 | # SQLAlchemy 2.0 style 13 | class Base(DeclarativeBase): 14 | pass 15 | 16 | 17 | engine = create_engine('sqlite:///one_to_one_v2.db', echo=False) 18 | 19 | 20 | # ============================================ 21 | # Пример 1: Пользователь и Профиль 22 | # ============================================ 23 | 24 | class User(Base): 25 | """Пользователь""" 26 | __tablename__ = 'users' 27 | 28 | id: Mapped[int] = mapped_column(primary_key=True) 29 | username: Mapped[str] = mapped_column(String(50), unique=True) 30 | email: Mapped[str] = mapped_column(String(100), unique=True) 31 | 32 | # One-to-One связь 33 | profile: Mapped[Optional["UserProfile"]] = relationship( 34 | back_populates="user", 35 | cascade="all, delete-orphan" 36 | ) 37 | 38 | class UserProfile(Base): 39 | """Профиль пользователя""" 40 | __tablename__ = 'user_profiles' 41 | 42 | id: Mapped[int] = mapped_column(primary_key=True) 43 | user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), unique=True) 44 | 45 | first_name: Mapped[Optional[str]] = mapped_column(String(50)) 46 | last_name: Mapped[Optional[str]] = mapped_column(String(50)) 47 | bio: Mapped[Optional[str]] 48 | avatar_url: Mapped[Optional[str]] = mapped_column(String(200)) 49 | birth_date: Mapped[Optional[date]] 50 | 51 | # Обратная связь 52 | user: Mapped["User"] = relationship(back_populates="profile") 53 | 54 | 55 | # ============================================ 56 | # Демонстрация 57 | # ============================================ 58 | 59 | def demo_user_profile(): 60 | """Демонстрация связи User-Profile""" 61 | print("\n" + "=" * 50) 62 | print("Demo: User and Profile (1-1) - SQLAlchemy 2.0") 63 | print("=" * 50) 64 | 65 | with Session(engine) as session: 66 | # 1. Создание пользователя с профилем 67 | print("\n1. Creating user with profile...") 68 | user = User( 69 | username="john_doe", 70 | email="john@example.com", 71 | profile=UserProfile( 72 | first_name="John", 73 | last_name="Doe", 74 | bio="Software Engineer", 75 | birth_date=date(1990, 5, 15) 76 | ) 77 | ) 78 | 79 | session.add(user) 80 | session.commit() 81 | 82 | session.refresh(user) 83 | print(f"User id: {user.id}") 84 | print(f"✓ Created: {user.username}") 85 | print(f"✓ Profile: {user.profile.first_name} {user.profile.last_name}") 86 | 87 | # 2. Чтение с использованием selectinload 88 | print("\n2. Reading user with profile (using selectinload)...") 89 | stmt = select(User).options(selectinload(User.profile)).where(User.username == "john_doe") 90 | user = session.execute(stmt).scalar_one() 91 | print(f"User: {user.username}") 92 | if user.profile: 93 | print(f"Profile: {user.profile.first_name} {user.profile.last_name}") 94 | print(f"Bio: {user.profile.bio}") 95 | 96 | # 3. Обратная связь 97 | print("\n3. Accessing user from profile...") 98 | stmt = select(UserProfile).options(selectinload(UserProfile.user)) 99 | profile = session.execute(stmt).scalars().first() 100 | print(f"Profile belongs to: {profile.user.username}") 101 | 102 | # 4. Каскадное удаление 103 | print("\n4. Testing cascade delete...") 104 | user_id = user.id 105 | session.delete(user) 106 | session.commit() 107 | 108 | stmt = select(UserProfile).where(UserProfile.user_id == user_id) 109 | profile_exists = session.execute(stmt).first() is not None 110 | print(f"Profile exists after user deletion: {profile_exists}") 111 | print("✓ Cascade delete worked!") 112 | 113 | 114 | if __name__ == "__main__": 115 | print("=" * 50) 116 | print("SQLAlchemy 2.0 One-to-One Relationship") 117 | print("=" * 50) 118 | 119 | Base.metadata.drop_all(engine) 120 | Base.metadata.create_all(engine) 121 | 122 | demo_user_profile() 123 | 124 | print("\n" + "=" * 50) 125 | print("Completed! Database: one_to_one_v2.db") 126 | print("=" * 50) 127 | -------------------------------------------------------------------------------- /example_01/connection.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 01: Подключение к базе данных (SQLAlchemy 2.0) 3 | Демонстрирует различные способы подключения к БД с помощью SQLAlchemy 2.0 4 | """ 5 | 6 | from sqlalchemy import create_engine, text, select 7 | from sqlalchemy.orm import DeclarativeBase, Session 8 | from contextlib import contextmanager 9 | 10 | 11 | # ============================================ 12 | # 1. SQLite подключение 13 | # ============================================ 14 | 15 | def sqlite_connection_example(): 16 | """Пример подключения к SQLite""" 17 | print("=" * 50) 18 | print("SQLite Connection Example") 19 | print("=" * 50) 20 | 21 | # Файловая база данных 22 | engine = create_engine('sqlite:///example.db', echo=True) 23 | 24 | # Проверка подключения 25 | try: 26 | with engine.connect() as connection: 27 | result = connection.execute(text("SELECT 'Hello SQLite' as message")) 28 | row = result.fetchone() 29 | print(f"\n✓ Connection successful: {row[0]}\n") 30 | except Exception as e: 31 | print(f"\n✗ Connection failed: {e}\n") 32 | 33 | return engine 34 | 35 | 36 | # ============================================ 37 | # 2. Session Management (SQLAlchemy 2.0) 38 | # ============================================ 39 | 40 | def session_example(): 41 | """Пример работы с сессиями в SQLAlchemy 2.0""" 42 | print("=" * 50) 43 | print("Session Management Example") 44 | print("=" * 50) 45 | 46 | engine = create_engine('sqlite:///example.db', echo=False) 47 | 48 | # Способ 1: Context Manager (рекомендуется) 49 | print("\n1. Context manager (recommended):") 50 | with Session(engine) as session: 51 | print(" - Session opened") 52 | # Работа с данными 53 | # session.add(...) 54 | # session.commit() 55 | print(" - Work done") 56 | print(" - Session will be closed automatically") 57 | 58 | # Способ 2: Custom context manager 59 | @contextmanager 60 | def get_session(): 61 | session = Session(engine) 62 | try: 63 | yield session 64 | session.commit() 65 | print(" - Changes committed") 66 | except Exception: 67 | session.rollback() 68 | print(" - Changes rolled back") 69 | raise 70 | finally: 71 | session.close() 72 | print(" - Session closed") 73 | 74 | print("\n2. Custom context manager with auto-commit:") 75 | with get_session() as session: 76 | print(" - Session opened") 77 | # Работа с данными 78 | print(" - Work done") 79 | 80 | 81 | # ============================================ 82 | # 3. Connection pooling 83 | # ============================================ 84 | 85 | def connection_pooling_example(): 86 | """Пример настройки пула соединений""" 87 | print("=" * 50) 88 | print("Connection Pooling Example") 89 | print("=" * 50) 90 | 91 | engine = create_engine( 92 | 'sqlite:///example.db', 93 | pool_size=5, 94 | max_overflow=10, 95 | pool_timeout=30, 96 | pool_recycle=3600, 97 | pool_pre_ping=True, 98 | echo=False 99 | ) 100 | 101 | print("\nPool configuration:") 102 | print(f" - Pool size: 5 connections") 103 | print(f" - Max overflow: 10 connections") 104 | print(f" - Total max: 15 connections") 105 | 106 | # Использование пула 107 | print("\nUsing connection pool:") 108 | for i in range(3): 109 | with engine.connect() as conn: 110 | result = conn.execute(text("SELECT 1")) 111 | print(f" Connection {i + 1}: {result.fetchone()[0]}") 112 | 113 | 114 | # ============================================ 115 | # 4. Database URL с параметрами 116 | # ============================================ 117 | 118 | def database_url_examples(): 119 | """Примеры различных Database URL""" 120 | print("=" * 50) 121 | print("Database URL Examples") 122 | print("=" * 50) 123 | 124 | examples = [ 125 | ("SQLite (file)", "sqlite:///myapp.db"), 126 | ("SQLite (memory)", "sqlite:///:memory:"), 127 | ("PostgreSQL", "postgresql://user:password@localhost/mydatabase"), 128 | ("PostgreSQL (port)", "postgresql://user:password@localhost:5432/mydatabase"), 129 | ("PostgreSQL (driver)", "postgresql+psycopg2://user:password@localhost/mydatabase"), 130 | ("MySQL", "mysql://user:password@localhost/mydatabase"), 131 | ("MySQL (PyMySQL)", "mysql+pymysql://user:password@localhost/mydatabase"), 132 | ] 133 | 134 | print("\nDatabase URL format:") 135 | print("dialect+driver://username:password@host:port/database\n") 136 | 137 | for name, url in examples: 138 | print(f"{name:25} {url}") 139 | 140 | 141 | # ============================================ 142 | # Main 143 | # ============================================ 144 | 145 | if __name__ == "__main__": 146 | # 1. SQLite подключение 147 | engine = sqlite_connection_example() 148 | 149 | # 2. Session management 150 | session_example() 151 | 152 | # 3. Connection pooling 153 | connection_pooling_example() 154 | 155 | # 4. Database URL examples 156 | database_url_examples() 157 | 158 | print("\n" + "=" * 50) 159 | print("All examples completed!") 160 | print("=" * 50) 161 | -------------------------------------------------------------------------------- /example_10/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts. 5 | # this is typically a path given in POSIX (e.g. forward slashes) 6 | # format, relative to the token %(here)s which refers to the location of this 7 | # ini file 8 | script_location = %(here)s/alembic 9 | 10 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 11 | # Uncomment the line below if you want the files to be prepended with date and time 12 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 13 | # for all available tokens 14 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 15 | 16 | # sys.path path, will be prepended to sys.path if present. 17 | # defaults to the current working directory. for multiple paths, the path separator 18 | # is defined by "path_separator" below. 19 | prepend_sys_path = . 20 | 21 | 22 | # timezone to use when rendering the date within the migration file 23 | # as well as the filename. 24 | # If specified, requires the tzdata library which can be installed by adding 25 | # `alembic[tz]` to the pip requirements. 26 | # string value is passed to ZoneInfo() 27 | # leave blank for localtime 28 | # timezone = 29 | 30 | # max length of characters to apply to the "slug" field 31 | # truncate_slug_length = 40 32 | 33 | # set to 'true' to run the environment during 34 | # the 'revision' command, regardless of autogenerate 35 | # revision_environment = false 36 | 37 | # set to 'true' to allow .pyc and .pyo files without 38 | # a source .py file to be detected as revisions in the 39 | # versions/ directory 40 | # sourceless = false 41 | 42 | # version location specification; This defaults 43 | # to /versions. When using multiple version 44 | # directories, initial revisions must be specified with --version-path. 45 | # The path separator used here should be the separator specified by "path_separator" 46 | # below. 47 | # version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions 48 | 49 | # path_separator; This indicates what character is used to split lists of file 50 | # paths, including version_locations and prepend_sys_path within configparser 51 | # files such as alembic.ini. 52 | # The default rendered in new alembic.ini files is "os", which uses os.pathsep 53 | # to provide os-dependent path splitting. 54 | # 55 | # Note that in order to support legacy alembic.ini files, this default does NOT 56 | # take place if path_separator is not present in alembic.ini. If this 57 | # option is omitted entirely, fallback logic is as follows: 58 | # 59 | # 1. Parsing of the version_locations option falls back to using the legacy 60 | # "version_path_separator" key, which if absent then falls back to the legacy 61 | # behavior of splitting on spaces and/or commas. 62 | # 2. Parsing of the prepend_sys_path option falls back to the legacy 63 | # behavior of splitting on spaces, commas, or colons. 64 | # 65 | # Valid values for path_separator are: 66 | # 67 | # path_separator = : 68 | # path_separator = ; 69 | # path_separator = space 70 | # path_separator = newline 71 | # 72 | # Use os.pathsep. Default configuration used for new projects. 73 | path_separator = os 74 | 75 | # set to 'true' to search source files recursively 76 | # in each "version_locations" directory 77 | # new in Alembic version 1.10 78 | # recursive_version_locations = false 79 | 80 | # the output encoding used when revision files 81 | # are written from script.py.mako 82 | # output_encoding = utf-8 83 | 84 | # database URL. This is consumed by the user-maintained env.py script only. 85 | # other means of configuring database URLs may be customized within the env.py 86 | # file. 87 | sqlalchemy.url = sqlite:///blog.db 88 | 89 | 90 | [post_write_hooks] 91 | # post_write_hooks defines scripts or Python functions that are run 92 | # on newly generated revision scripts. See the documentation for further 93 | # detail and examples 94 | 95 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 96 | # hooks = black 97 | # black.type = console_scripts 98 | # black.entrypoint = black 99 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 100 | 101 | # lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module 102 | # hooks = ruff 103 | # ruff.type = module 104 | # ruff.module = ruff 105 | # ruff.options = check --fix REVISION_SCRIPT_FILENAME 106 | 107 | # Alternatively, use the exec runner to execute a binary found on your PATH 108 | # hooks = ruff 109 | # ruff.type = exec 110 | # ruff.executable = ruff 111 | # ruff.options = check --fix REVISION_SCRIPT_FILENAME 112 | 113 | # Logging configuration. This is also consumed by the user-maintained 114 | # env.py script only. 115 | [loggers] 116 | keys = root,sqlalchemy,alembic 117 | 118 | [handlers] 119 | keys = console 120 | 121 | [formatters] 122 | keys = generic 123 | 124 | [logger_root] 125 | level = WARNING 126 | handlers = console 127 | qualname = 128 | 129 | [logger_sqlalchemy] 130 | level = WARNING 131 | handlers = 132 | qualname = sqlalchemy.engine 133 | 134 | [logger_alembic] 135 | level = INFO 136 | handlers = 137 | qualname = alembic 138 | 139 | [handler_console] 140 | class = StreamHandler 141 | args = (sys.stderr,) 142 | level = NOTSET 143 | formatter = generic 144 | 145 | [formatter_generic] 146 | format = %(levelname)-5.5s [%(name)s] %(message)s 147 | datefmt = %H:%M:%S 148 | -------------------------------------------------------------------------------- /example_07/crud_operations.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 07: CRUD операции (SQLAlchemy 2.0) 3 | Демонстрирует Create, Read, Update, Delete операции с современным синтаксисом 4 | """ 5 | 6 | from sqlalchemy import create_engine, String, select, update, delete, func 7 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session 8 | from datetime import datetime, timezone 9 | from typing import Optional 10 | 11 | 12 | # SQLAlchemy 2.0 style 13 | class Base(DeclarativeBase): 14 | pass 15 | 16 | 17 | engine = create_engine('sqlite:///crud.db', echo=False) 18 | 19 | 20 | class User(Base): 21 | """Модель пользователя""" 22 | __tablename__ = 'users' 23 | 24 | id: Mapped[int] = mapped_column(primary_key=True) 25 | username: Mapped[str] = mapped_column(String(50), unique=True) 26 | email: Mapped[Optional[str]] = mapped_column(String(100), unique=True) 27 | age: Mapped[Optional[int]] 28 | created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc)) 29 | 30 | 31 | # CREATE 32 | def demo_create(): 33 | """Создание записей""" 34 | print("\n" + "=" * 50) 35 | print("CREATE Operations") 36 | print("=" * 50) 37 | 38 | with Session(engine) as session: 39 | # 1. Одна запись 40 | user1 = User(username="alice", email="alice@example.com", age=25) 41 | session.add(user1) 42 | session.commit() 43 | print(f"✓ Created user: {user1.username} (ID: {user1.id})") 44 | 45 | # 2. Несколько записей 46 | users = [ 47 | User(username="bob", email="bob@example.com", age=30), 48 | User(username="charlie", email="charlie@example.com", age=28), 49 | ] 50 | session.add_all(users) 51 | session.commit() 52 | print(f"✓ Created {len(users)} users") 53 | 54 | 55 | # READ 56 | def demo_read(): 57 | """Чтение записей""" 58 | print("\n" + "=" * 50) 59 | print("READ Operations") 60 | print("=" * 50) 61 | 62 | with Session(engine) as session: 63 | # 1. Все записи 64 | stmt = select(User) 65 | users = session.execute(stmt).scalars().all() 66 | print(f"\n1. All users ({len(users)}):") 67 | for user in users: 68 | print(f" {user.username} - {user.email}") 69 | 70 | # 2. Первая запись 71 | stmt = select(User) 72 | user = session.execute(stmt).scalars().first() 73 | print(f"\n2. First user: {user.username}") 74 | 75 | # 3. По ID 76 | user = session.get(User, 1) 77 | print(f"\n3. User by ID(1): {user.username if user else 'Not found'}") 78 | 79 | # 4. С фильтром 80 | stmt = select(User).where(User.username == "alice") 81 | user = session.execute(stmt).scalars().first() 82 | print(f"\n4. Filter by username: {user.username}") 83 | 84 | # 5. С условием 85 | stmt = select(User).where(User.age >= 28) 86 | users = session.execute(stmt).scalars().all() 87 | print(f"\n5. Users with age >= 28: {[u.username for u in users]}") 88 | 89 | # 6. Лимит 90 | stmt = select(User).limit(2) 91 | users = session.execute(stmt).scalars().all() 92 | print(f"\n6. First 2 users: {[u.username for u in users]}") 93 | 94 | # 7. Подсчет 95 | stmt = select(func.count()).select_from(User) 96 | count = session.execute(stmt).scalar() 97 | print(f"\n7. Total users count: {count}") 98 | 99 | 100 | # UPDATE 101 | def demo_update(): 102 | """Обновление записей""" 103 | print("\n" + "=" * 50) 104 | print("UPDATE Operations") 105 | print("=" * 50) 106 | 107 | with Session(engine) as session: 108 | # 1. Обновление через объект 109 | stmt = select(User).where(User.username == "alice") 110 | user = session.execute(stmt).scalars().first() 111 | old_email = user.email 112 | user.email = "alice_new@example.com" 113 | session.commit() 114 | print(f"✓ Updated {user.username} email: {old_email} -> {user.email}") 115 | 116 | # 2. Массовое обновление 117 | stmt = update(User).where(User.age < 30).values(age=User.age + 1) 118 | result = session.execute(stmt) 119 | session.commit() 120 | print(f"✓ Updated {result.rowcount} users (age + 1)") 121 | 122 | 123 | # DELETE 124 | def demo_delete(): 125 | """Удаление записей""" 126 | print("\n" + "=" * 50) 127 | print("DELETE Operations") 128 | print("=" * 50) 129 | 130 | with Session(engine) as session: 131 | # 1. Удаление через объект 132 | stmt = select(User).where(User.username == "bob") 133 | user = session.execute(stmt).scalars().first() 134 | session.delete(user) 135 | session.commit() 136 | print(f"✓ Deleted user: bob") 137 | 138 | # 2. Массовое удаление 139 | stmt = delete(User).where(User.age > 100) 140 | result = session.execute(stmt) 141 | session.commit() 142 | print(f"✓ Deleted {result.rowcount} users with age > 100") 143 | 144 | 145 | if __name__ == "__main__": 146 | print("=" * 50) 147 | print("SQLAlchemy 2.0 CRUD Operations") 148 | print("=" * 50) 149 | 150 | Base.metadata.drop_all(engine) 151 | Base.metadata.create_all(engine) 152 | 153 | demo_create() 154 | demo_read() 155 | demo_update() 156 | demo_delete() 157 | 158 | print("\n" + "=" * 50) 159 | print("Completed!") 160 | print("=" * 50) 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SQLAlchemy 2.0: Современный синтаксис 2 | 3 | ## Важно! Используем SQLAlchemy 2.0 4 | 5 | Все примеры в этом конспекте обновлены для SQLAlchemy 2.0, который вводит современный, типобезопасный подход к работе с ORM. 6 | 7 | ### Ключевые изменения в 2.0 8 | 9 | #### 1. DeclarativeBase вместо declarative_base() 10 | 11 | **Старый способ (1.x):** 12 | ```python 13 | from sqlalchemy.ext.declarative import declarative_base 14 | Base = declarative_base() 15 | ``` 16 | 17 | **Новый способ (2.0):** 18 | ```python 19 | from sqlalchemy.orm import DeclarativeBase 20 | 21 | class Base(DeclarativeBase): 22 | pass 23 | ``` 24 | 25 | #### 2. mapped_column и Mapped для типобезопасности 26 | 27 | **Старый способ:** 28 | ```python 29 | from sqlalchemy import Column, Integer, String 30 | 31 | class User(Base): 32 | __tablename__ = 'users' 33 | id = Column(Integer, primary_key=True) 34 | username = Column(String(50)) 35 | email = Column(String(100), nullable=True) 36 | ``` 37 | 38 | **Новый способ:** 39 | ```python 40 | from sqlalchemy.orm import Mapped, mapped_column 41 | from typing import Optional 42 | 43 | class User(Base): 44 | __tablename__ = 'users' 45 | 46 | id: Mapped[int] = mapped_column(primary_key=True) 47 | username: Mapped[str] = mapped_column(String(50)) 48 | email: Mapped[Optional[str]] = mapped_column(String(100)) 49 | ``` 50 | 51 | #### 3. select() вместо Query API 52 | 53 | **Старый способ:** 54 | ```python 55 | # Все записи 56 | users = session.query(User).all() 57 | 58 | # С фильтром 59 | user = session.query(User).filter_by(username="alice").first() 60 | 61 | # С условием 62 | users = session.query(User).filter(User.age > 18).all() 63 | ``` 64 | 65 | **Новый способ:** 66 | ```python 67 | from sqlalchemy import select 68 | 69 | # Все записи 70 | stmt = select(User) 71 | users = session.execute(stmt).scalars().all() 72 | 73 | # С фильтром 74 | stmt = select(User).where(User.username == "alice") 75 | user = session.execute(stmt).scalars().first() 76 | 77 | # С условием 78 | stmt = select(User).where(User.age > 18) 79 | users = session.execute(stmt).scalars().all() 80 | ``` 81 | 82 | #### 4. Session как context manager 83 | 84 | **Старый способ:** 85 | ```python 86 | from sqlalchemy.orm import sessionmaker 87 | Session = sessionmaker(bind=engine) 88 | session = Session() 89 | try: 90 | # работа с данными 91 | session.commit() 92 | finally: 93 | session.close() 94 | ``` 95 | 96 | **Новый способ:** 97 | ```python 98 | from sqlalchemy.orm import Session 99 | 100 | with Session(engine) as session: 101 | # работа с данными 102 | session.commit() 103 | ``` 104 | 105 | #### 5. selectinload вместо joinedload 106 | 107 | **Старый способ:** 108 | ```python 109 | from sqlalchemy.orm import joinedload 110 | 111 | users = session.query(User).options(joinedload(User.posts)).all() 112 | ``` 113 | 114 | **Новый способ:** 115 | ```python 116 | from sqlalchemy import select 117 | from sqlalchemy.orm import selectinload 118 | 119 | stmt = select(User).options(selectinload(User.posts)) 120 | users = session.execute(stmt).scalars().all() 121 | ``` 122 | 123 | **Преимущества selectinload:** 124 | - Выполняет отдельный оптимизированный запрос для связанных объектов 125 | - Избегает дублирования данных (которое происходит с JOIN) 126 | - Более эффективен для связей one-to-many и many-to-many 127 | 128 | #### 6. Relationships с типами 129 | 130 | **Новый способ:** 131 | ```python 132 | from typing import List, Optional 133 | 134 | class User(Base): 135 | __tablename__ = 'users' 136 | 137 | id: Mapped[int] = mapped_column(primary_key=True) 138 | 139 | # One-to-Many 140 | posts: Mapped[List["Post"]] = relationship(back_populates="author") 141 | 142 | # One-to-One 143 | profile: Mapped[Optional["Profile"]] = relationship(back_populates="user") 144 | 145 | 146 | class Post(Base): 147 | __tablename__ = 'posts' 148 | 149 | id: Mapped[int] = mapped_column(primary_key=True) 150 | author_id: Mapped[int] = mapped_column(ForeignKey('users.id')) 151 | 152 | # Many-to-One 153 | author: Mapped["User"] = relationship(back_populates="posts") 154 | ``` 155 | 156 | #### 7. Update и Delete с новым синтаксисом 157 | 158 | **Старый способ:** 159 | ```python 160 | session.query(User).filter(User.age < 18).update({"status": "minor"}) 161 | session.query(User).filter(User.id == 5).delete() 162 | ``` 163 | 164 | **Новый способ:** 165 | ```python 166 | from sqlalchemy import update, delete 167 | 168 | stmt = update(User).where(User.age < 18).values(status="minor") 169 | session.execute(stmt) 170 | 171 | stmt = delete(User).where(User.id == 5) 172 | session.execute(stmt) 173 | ``` 174 | 175 | ### Преимущества SQLAlchemy 2.0 176 | 177 | 1. **Type Safety** - полная поддержка type hints и проверка типов 178 | 2. **Consistency** - единообразный API для всех операций 179 | 3. **Performance** - оптимизированная генерация SQL 180 | 4. **Better IDE Support** - автодополнение и подсказки в IDE 181 | 5. **Async Support** - встроенная поддержка asyncio 182 | 6. **Clear Errors** - более понятные сообщения об ошибках 183 | 184 | ### Миграция с 1.x на 2.0 185 | 186 | Если у вас есть код на SQLAlchemy 1.x: 187 | 188 | 1. Обновите импорты: `DeclarativeBase`, `Mapped`, `mapped_column` 189 | 2. Замените `Column` на `mapped_column` с типами 190 | 3. Замените `session.query()` на `select()` 191 | 4. Используйте `session.execute().scalars()` для получения объектов 192 | 5. Замените `joinedload` на `selectinload` где возможно 193 | 194 | **См. полный гид по миграции:** [SQLAlchemy_2.0_Migration_Guide.md](SQLAlchemy_2.0_Migration_Guide.md) 195 | 196 | --- 197 | -------------------------------------------------------------------------------- /example_04/one_to_many_v2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 04: Связь One-to-Many (SQLAlchemy 2.0) 3 | Демонстрирует реализацию связи один-ко-многим с современным синтаксисом 4 | """ 5 | 6 | from sqlalchemy import create_engine, String, ForeignKey, Text, select 7 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session, selectinload 8 | from datetime import datetime, timezone 9 | from typing import Optional, List 10 | 11 | 12 | class Base(DeclarativeBase): 13 | pass 14 | 15 | 16 | engine = create_engine('sqlite:///one_to_many_v2.db', echo=False) 17 | 18 | 19 | # ============================================ 20 | # Блог: User, Post, Comment 21 | # ============================================ 22 | 23 | class BlogUser(Base): 24 | """Пользователь блога""" 25 | __tablename__ = 'blog_users' 26 | 27 | id: Mapped[int] = mapped_column(primary_key=True) 28 | username: Mapped[str] = mapped_column(String(50), unique=True) 29 | email: Mapped[Optional[str]] = mapped_column(String(100), unique=True) 30 | 31 | # One-to-Many отношения 32 | posts: Mapped[List["BlogPost"]] = relationship( 33 | back_populates="author", 34 | cascade="all, delete-orphan" 35 | ) 36 | comments: Mapped[List["Comment"]] = relationship( 37 | back_populates="author", 38 | cascade="all, delete-orphan" 39 | ) 40 | 41 | 42 | class BlogPost(Base): 43 | """Пост в блоге""" 44 | __tablename__ = 'blog_posts' 45 | 46 | id: Mapped[int] = mapped_column(primary_key=True) 47 | title: Mapped[str] = mapped_column(String(200)) 48 | content: Mapped[Optional[str]] = mapped_column(Text) 49 | created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc)) 50 | is_published: Mapped[bool] = mapped_column(default=False) 51 | 52 | # Many-to-One: много постов - один автор 53 | author_id: Mapped[int] = mapped_column(ForeignKey('blog_users.id')) 54 | author: Mapped["BlogUser"] = relationship(back_populates="posts") 55 | 56 | # One-to-Many: один пост - много комментариев 57 | comments: Mapped[List["Comment"]] = relationship( 58 | back_populates="post", 59 | cascade="all, delete-orphan" 60 | ) 61 | 62 | 63 | class Comment(Base): 64 | """Комментарий""" 65 | __tablename__ = 'comments' 66 | 67 | id: Mapped[int] = mapped_column(primary_key=True) 68 | content: Mapped[str] = mapped_column(Text) 69 | created_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc)) 70 | 71 | author_id: Mapped[int] = mapped_column(ForeignKey('blog_users.id')) 72 | author: Mapped["BlogUser"] = relationship(back_populates="comments") 73 | 74 | post_id: Mapped[int] = mapped_column(ForeignKey('blog_posts.id')) 75 | post: Mapped["BlogPost"] = relationship(back_populates="comments") 76 | 77 | 78 | # ============================================ 79 | # Демонстрация 80 | # ============================================ 81 | 82 | def demo_blog(): 83 | """Демонстрация блога с SQLAlchemy 2.0""" 84 | print("\n" + "=" * 50) 85 | print("Demo: Blog (Users, Posts, Comments) - SQLAlchemy 2.0") 86 | print("=" * 50) 87 | 88 | with Session(engine) as session: 89 | # Создание пользователей 90 | user1 = BlogUser(username="alice", email="alice@blog.com") 91 | user2 = BlogUser(username="bob", email="bob@blog.com") 92 | 93 | # Создание постов 94 | post1 = BlogPost( 95 | title="Introduction to Python", 96 | content="Python is amazing...", 97 | author=user1, 98 | is_published=True 99 | ) 100 | 101 | post2 = BlogPost( 102 | title="SQLAlchemy 2.0 Guide", 103 | content="Modern SQLAlchemy...", 104 | author=user1, 105 | is_published=True 106 | ) 107 | 108 | # Комментарии 109 | Comment(content="Great post!", author=user2, post=post1) 110 | Comment(content="Very helpful!", author=user2, post=post1) 111 | Comment(content="Thanks!", author=user1, post=post1) 112 | 113 | session.add_all([user1, user2]) 114 | session.commit() 115 | 116 | # Чтение с selectinload 117 | print("\n1. Users with their posts (using selectinload):") 118 | stmt = select(BlogUser).options( 119 | selectinload(BlogUser.posts) 120 | ) 121 | # Без selectinload - не будет постов 122 | users = session.execute(stmt).scalars().all() 123 | 124 | for user in users: 125 | print(f"\n{user.username} has {len(user.posts)} posts") 126 | for post in user.posts: 127 | print(f" - '{post.title}'") 128 | 129 | # Посты с комментариями 130 | print("\n2. Posts with comments (using selectinload):") 131 | stmt = select(BlogPost).options( 132 | selectinload(BlogPost.author), 133 | selectinload(BlogPost.comments).selectinload(Comment.author) 134 | ).where(BlogPost.is_published == True) 135 | 136 | posts = session.execute(stmt).scalars().all() 137 | 138 | for post in posts: 139 | print(f"\n'{post.title}' by {post.author.username}") 140 | print(f" Comments ({len(post.comments)}):") 141 | for comment in post.comments: 142 | print(f" - {comment.author.username}: {comment.content}") 143 | 144 | 145 | if __name__ == "__main__": 146 | print("=" * 50) 147 | print("SQLAlchemy 2.0 One-to-Many Relationship") 148 | print("=" * 50) 149 | 150 | Base.metadata.drop_all(engine) 151 | Base.metadata.create_all(engine) 152 | 153 | demo_blog() 154 | 155 | print("\n" + "=" * 50) 156 | print("Completed! Database: one_to_many_v2.db") 157 | print("=" * 50) 158 | -------------------------------------------------------------------------------- /example_08/QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # Быстрый старт с Async SQLAlchemy 2 | 3 | ## Установка 4 | 5 | ```bash 6 | # Для SQLite (проще для начала) 7 | pip install sqlalchemy[asyncio] aiosqlite 8 | 9 | # Для PostgreSQL (production) 10 | pip install sqlalchemy[asyncio] asyncpg 11 | ``` 12 | 13 | ## 1. Запуск примера с SQLite 14 | 15 | ```bash 16 | cd example_08 17 | python async_sqlite.py 18 | ``` 19 | 20 | **Что произойдет:** 21 | - Создастся файл `async_example.db` 22 | - Выполнятся все async операции 23 | - Вы увидите SQL запросы в консоли 24 | 25 | ## 2. Запуск примера с PostgreSQL 26 | 27 | ### Подготовка 28 | 29 | 1. Установите PostgreSQL: 30 | ```bash 31 | # Ubuntu/Debian 32 | sudo apt-get install postgresql postgresql-contrib 33 | 34 | # macOS 35 | brew install postgresql 36 | ``` 37 | 38 | 2. Создайте базу данных: 39 | ```bash 40 | sudo -u postgres psql 41 | CREATE DATABASE async_db; 42 | CREATE USER user WITH PASSWORD 'password'; 43 | GRANT ALL PRIVILEGES ON DATABASE async_db TO user; 44 | \q 45 | ``` 46 | 47 | 3. Обновите URL в `async_postgres.py`: 48 | ```python 49 | DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/async_db" 50 | ``` 51 | 52 | 4. Запустите: 53 | ```bash 54 | python async_postgres.py 55 | ``` 56 | 57 | ## 3. Простейший пример (скопируйте и запустите) 58 | 59 | Создайте файл `my_async_example.py`: 60 | 61 | ```python 62 | import asyncio 63 | from sqlalchemy import String, select 64 | from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession 65 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 66 | 67 | 68 | class Base(DeclarativeBase): 69 | pass 70 | 71 | 72 | class User(Base): 73 | __tablename__ = 'users' 74 | id: Mapped[int] = mapped_column(primary_key=True) 75 | name: Mapped[str] = mapped_column(String(50)) 76 | 77 | 78 | async def main(): 79 | # 1. Создать engine 80 | engine = create_async_engine('sqlite+aiosqlite:///test.db') 81 | 82 | # 2. Создать таблицы 83 | async with engine.begin() as conn: 84 | await conn.run_sync(Base.metadata.create_all) 85 | 86 | # 3. Создать session maker 87 | AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False) 88 | 89 | # 4. Добавить пользователя 90 | async with AsyncSessionLocal() as session: 91 | user = User(name="Alice") 92 | session.add(user) 93 | await session.commit() 94 | print(f"Created user: {user.name} (ID: {user.id})") 95 | 96 | # 5. Прочитать пользователя 97 | async with AsyncSessionLocal() as session: 98 | result = await session.execute(select(User)) 99 | users = result.scalars().all() 100 | print(f"Found {len(users)} users: {[u.name for u in users]}") 101 | 102 | # 6. Закрыть engine 103 | await engine.dispose() 104 | 105 | 106 | if __name__ == "__main__": 107 | asyncio.run(main()) 108 | ``` 109 | 110 | Запустите: 111 | ```bash 112 | python my_async_example.py 113 | ``` 114 | 115 | ## 4. Интеграция с FastAPI 116 | 117 | Создайте файл `fastapi_example.py`: 118 | 119 | ```python 120 | from fastapi import FastAPI, Depends 121 | from sqlalchemy import String, select 122 | from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession 123 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 124 | 125 | 126 | class Base(DeclarativeBase): 127 | pass 128 | 129 | 130 | class User(Base): 131 | __tablename__ = 'users' 132 | id: Mapped[int] = mapped_column(primary_key=True) 133 | name: Mapped[str] = mapped_column(String(50)) 134 | 135 | 136 | # Настройка БД 137 | engine = create_async_engine('sqlite+aiosqlite:///fastapi.db') 138 | AsyncSessionLocal = async_sessionmaker(engine, expire_on_commit=False) 139 | 140 | 141 | # FastAPI app 142 | app = FastAPI() 143 | 144 | 145 | # Dependency 146 | async def get_db(): 147 | async with AsyncSessionLocal() as session: 148 | yield session 149 | 150 | 151 | # Startup event 152 | @app.on_event("startup") 153 | async def startup(): 154 | async with engine.begin() as conn: 155 | await conn.run_sync(Base.metadata.create_all) 156 | 157 | 158 | # Routes 159 | @app.post("/users/") 160 | async def create_user(name: str, db: AsyncSession = Depends(get_db)): 161 | user = User(name=name) 162 | db.add(user) 163 | await db.commit() 164 | return {"id": user.id, "name": user.name} 165 | 166 | 167 | @app.get("/users/") 168 | async def list_users(db: AsyncSession = Depends(get_db)): 169 | result = await db.execute(select(User)) 170 | users = result.scalars().all() 171 | return [{"id": u.id, "name": u.name} for u in users] 172 | 173 | 174 | @app.get("/users/{user_id}") 175 | async def get_user(user_id: int, db: AsyncSession = Depends(get_db)): 176 | user = await db.get(User, user_id) 177 | if not user: 178 | return {"error": "User not found"}, 404 179 | return {"id": user.id, "name": user.name} 180 | ``` 181 | 182 | Установите FastAPI и запустите: 183 | ```bash 184 | pip install fastapi uvicorn 185 | uvicorn fastapi_example:app --reload 186 | ``` 187 | 188 | Откройте http://localhost:8000/docs для Swagger UI. 189 | 190 | ## 5. Типичные ошибки 191 | 192 | ### ❌ Забыли await 193 | 194 | ```python 195 | # Неправильно 196 | user = session.get(User, 1) # Вернет coroutine, а не User 197 | 198 | # Правильно 199 | user = await session.get(User, 1) 200 | ``` 201 | 202 | ### ❌ Используете sync engine 203 | 204 | ```python 205 | # Неправильно 206 | engine = create_engine('sqlite:///db.db') 207 | 208 | # Правильно 209 | engine = create_async_engine('sqlite+aiosqlite:///db.db') 210 | ``` 211 | 212 | ### ❌ Забыли про relationships 213 | 214 | ```python 215 | # Неправильно - вызовет ошибку в async 216 | user.posts # LazyLoadError 217 | 218 | # Правильно 219 | stmt = select(User).options(selectinload(User.posts)) 220 | user = (await session.execute(stmt)).scalar_one() 221 | user.posts # Работает! 222 | ``` 223 | 224 | ## 6. Проверка работы 225 | 226 | После запуска любого примера проверьте созданную БД: 227 | 228 | ```bash 229 | # Для SQLite 230 | sqlite3 async_example.db 231 | > .tables 232 | > SELECT * FROM users; 233 | > .quit 234 | ``` 235 | 236 | ## Следующие шаги 237 | 238 | 1. Изучите [README.md](README.md) для полного понимания 239 | 2. Посмотрите `async_sqlite.py` для базовых примеров 240 | 3. Изучите `async_postgres.py` для production паттернов 241 | 4. Попробуйте интеграцию с FastAPI 242 | 5. Прочитайте официальную документацию SQLAlchemy Async 243 | 244 | ## Полезные ссылки 245 | 246 | - [SQLAlchemy Async Tutorial](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html) 247 | - [FastAPI with SQLAlchemy](https://fastapi.tiangolo.com/tutorial/sql-databases/) 248 | - [asyncpg Performance](https://magicstack.github.io/asyncpg/current/benchmarks.html) 249 | -------------------------------------------------------------------------------- /example_10/blog_app.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 10: Полное приложение - Блог 3 | Демонстрирует использование всех концепций SQLAlchemy 4 | """ 5 | 6 | from sqlalchemy.orm import sessionmaker 7 | from contextlib import contextmanager 8 | from models import Base, engine, User, Post, Comment, Tag 9 | 10 | # ============================================ 11 | # Настройка 12 | # ============================================ 13 | 14 | 15 | Session = sessionmaker(bind=engine) 16 | 17 | 18 | @contextmanager 19 | def get_session(): 20 | """Context manager для безопасной работы с сессией""" 21 | session = Session() 22 | try: 23 | yield session 24 | session.commit() 25 | except Exception: 26 | session.rollback() 27 | raise 28 | finally: 29 | session.close() 30 | 31 | 32 | 33 | 34 | 35 | # ============================================ 36 | # Бизнес-логика 37 | # ============================================ 38 | 39 | class BlogService: 40 | """Сервис для работы с блогом""" 41 | 42 | @staticmethod 43 | def create_user(username, email, password): 44 | """Создать пользователя""" 45 | with get_session() as session: 46 | user = User(username=username, email=email, password_hash=password) 47 | session.add(user) 48 | session.flush() # гарантируем, что БД присвоит первичный ключ до выхода 49 | return user.id 50 | 51 | @staticmethod 52 | def create_post(author_id, title, content, tags=None): 53 | """Создать пост""" 54 | with get_session() as session: 55 | slug = title.lower().replace(" ", "-") 56 | post = Post( 57 | author_id=author_id, 58 | title=title, 59 | slug=slug, 60 | content=content 61 | ) 62 | 63 | if tags: 64 | for tag_name in tags: 65 | tag = session.query(Tag).filter_by(name=tag_name).first() 66 | if not tag: 67 | tag = Tag(name=tag_name) 68 | post.tags.append(tag) 69 | 70 | session.add(post) 71 | session.flush() # чтобы post.id и связи были доступны сразу 72 | return post.id 73 | 74 | @staticmethod 75 | def publish_post(post_id): 76 | """Опубликовать пост""" 77 | with get_session() as session: 78 | post = session.query(Post).get(post_id) 79 | if post: 80 | post.is_published = True 81 | return True 82 | return False 83 | 84 | @staticmethod 85 | def add_comment(post_id, author_id, content): 86 | """Добавить комментарий""" 87 | with get_session() as session: 88 | comment = Comment( 89 | post_id=post_id, 90 | author_id=author_id, 91 | content=content 92 | ) 93 | session.add(comment) 94 | return comment.id 95 | 96 | @staticmethod 97 | def get_published_posts(): 98 | """Получить опубликованные посты""" 99 | with get_session() as session: 100 | return session.query(Post).filter_by(is_published=True).all() 101 | 102 | @staticmethod 103 | def get_post_with_details(post_id): 104 | """Получить пост с автором, комментариями и тегами""" 105 | with get_session() as session: 106 | post = session.query(Post).get(post_id) 107 | if post: 108 | # Увеличить счетчик просмотров 109 | post.view_count += 1 110 | session.commit() 111 | return post 112 | 113 | 114 | # ============================================ 115 | # Демонстрация 116 | # ============================================ 117 | 118 | def setup_database(): 119 | """Создать таблицы""" 120 | Base.metadata.drop_all(engine) 121 | Base.metadata.create_all(engine) 122 | print("✓ Database created") 123 | 124 | 125 | def populate_data(): 126 | """Заполнить тестовыми данными""" 127 | print("\n" + "=" * 50) 128 | print("Populating database...") 129 | print("=" * 50) 130 | 131 | # Создать пользователей 132 | alice_id = BlogService.create_user("alice", "alice@blog.com", "hash123") 133 | bob_id = BlogService.create_user("bob", "bob@blog.com", "hash456") 134 | print("✓ Created 2 users") 135 | 136 | # Создать посты 137 | post1_id = BlogService.create_post( 138 | alice_id, 139 | "Introduction to Python", 140 | "Python is an amazing language...", 141 | tags=["python", "tutorial", "beginner"] 142 | ) 143 | BlogService.publish_post(post1_id) 144 | 145 | post2_id = BlogService.create_post( 146 | alice_id, 147 | "SQLAlchemy Guide", 148 | "Learn how to use SQLAlchemy ORM...", 149 | tags=["python", "database", "sqlalchemy"] 150 | ) 151 | BlogService.publish_post(post2_id) 152 | 153 | post3_id = BlogService.create_post( 154 | bob_id, 155 | "Web Development Tips", 156 | "Best practices for web development...", 157 | tags=["web", "tutorial"] 158 | ) 159 | print("✓ Created 3 posts") 160 | 161 | # Добавить комментарии 162 | BlogService.add_comment(post1_id, bob_id, "Great post! Very helpful.") 163 | BlogService.add_comment(post1_id, alice_id, "Thank you!") 164 | BlogService.add_comment(post2_id, bob_id, "I love SQLAlchemy!") 165 | print("✓ Added 3 comments") 166 | 167 | 168 | def display_stats(): 169 | """Показать статистику""" 170 | print("\n" + "=" * 50) 171 | print("Blog Statistics") 172 | print("=" * 50) 173 | 174 | with get_session() as session: 175 | users_count = session.query(User).count() 176 | posts_count = session.query(Post).count() 177 | published_posts = session.query(Post).filter_by(is_published=True).count() 178 | comments_count = session.query(Comment).count() 179 | tags_count = session.query(Tag).count() 180 | 181 | print(f"\nUsers: {users_count}") 182 | print(f"Posts: {posts_count} ({published_posts} published)") 183 | print(f"Comments: {comments_count}") 184 | print(f"Tags: {tags_count}") 185 | 186 | 187 | def display_posts(): 188 | """Показать все посты""" 189 | print("\n" + "=" * 50) 190 | print("Published Posts") 191 | print("=" * 50) 192 | 193 | posts = BlogService.get_published_posts() 194 | 195 | for post in posts: 196 | with get_session() as session: 197 | post = session.merge(post) 198 | print(f"\n'{post.title}' by {post.author.username}") 199 | print(f"Tags: {', '.join(t.name for t in post.tags)}") 200 | print(f"Comments: {len(post.comments)}, Views: {post.view_count}") 201 | print(f"Created: {post.created_at.strftime('%Y-%m-%d %H:%M')}") 202 | 203 | 204 | if __name__ == "__main__": 205 | print("=" * 50) 206 | print("Blog Application Example") 207 | print("=" * 50) 208 | 209 | setup_database() 210 | populate_data() 211 | display_stats() 212 | display_posts() 213 | 214 | print("\n" + "=" * 50) 215 | print("Completed! Database: blog.db") 216 | print("=" * 50) 217 | -------------------------------------------------------------------------------- /example_05/many_to_many_v2.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 05: Связь Many-to-Many (SQLAlchemy 2.0) 3 | Демонстрирует реализацию связи многие-ко-многим с современным синтаксисом 4 | """ 5 | 6 | from sqlalchemy import create_engine, String, Table, Column, ForeignKey, select 7 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session, selectinload 8 | from datetime import datetime, timezone 9 | from typing import Optional, List 10 | 11 | 12 | class Base(DeclarativeBase): 13 | pass 14 | 15 | 16 | engine = create_engine('sqlite:///many_to_many_v2.db', echo=False) 17 | 18 | 19 | # ============================================ 20 | # Пример 1: Посты и Теги 21 | # ============================================ 22 | 23 | # Ассоциативная таблица 24 | post_tags = Table( 25 | 'post_tags', 26 | Base.metadata, 27 | Column('post_id', ForeignKey('posts.id'), primary_key=True), 28 | Column('tag_id', ForeignKey('tags.id'), primary_key=True) 29 | ) 30 | 31 | 32 | class Post(Base): 33 | """Пост""" 34 | __tablename__ = 'posts' 35 | 36 | id: Mapped[int] = mapped_column(primary_key=True) 37 | title: Mapped[str] = mapped_column(String(200)) 38 | content: Mapped[Optional[str]] 39 | 40 | # Many-to-Many через secondary 41 | tags: Mapped[List["Tag"]] = relationship( 42 | secondary=post_tags, 43 | back_populates="posts" 44 | ) 45 | 46 | 47 | class Tag(Base): 48 | """Тег""" 49 | __tablename__ = 'tags' 50 | 51 | id: Mapped[int] = mapped_column(primary_key=True) 52 | name: Mapped[str] = mapped_column(String(50), unique=True) 53 | 54 | # Обратная связь 55 | posts: Mapped[List["Post"]] = relationship( 56 | secondary=post_tags, 57 | back_populates="tags" 58 | ) 59 | 60 | 61 | # ============================================ 62 | # Пример 2: Студенты и Курсы (с доп. данными) 63 | # ============================================ 64 | 65 | class StudentCourse(Base): 66 | """Ассоциативная таблица с дополнительными данными""" 67 | __tablename__ = 'student_courses' 68 | 69 | student_id: Mapped[int] = mapped_column(ForeignKey('students.id'), primary_key=True) 70 | course_id: Mapped[int] = mapped_column(ForeignKey('courses.id'), primary_key=True) 71 | 72 | # Дополнительные данные 73 | enrolled_at: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc)) 74 | grade: Mapped[Optional[int]] 75 | completed: Mapped[bool] = mapped_column(default=False) 76 | 77 | # Relationships 78 | student: Mapped["Student"] = relationship(back_populates="course_links") 79 | course: Mapped["Course"] = relationship(back_populates="student_links") 80 | 81 | 82 | class Student(Base): 83 | """Студент""" 84 | __tablename__ = 'students' 85 | 86 | id: Mapped[int] = mapped_column(primary_key=True) 87 | name: Mapped[str] = mapped_column(String(100)) 88 | email: Mapped[Optional[str]] = mapped_column(String(100)) 89 | 90 | # Связь через ассоциативную модель 91 | course_links: Mapped[List["StudentCourse"]] = relationship(back_populates="student") 92 | 93 | 94 | class Course(Base): 95 | """Курс""" 96 | __tablename__ = 'courses' 97 | 98 | id: Mapped[int] = mapped_column(primary_key=True) 99 | name: Mapped[str] = mapped_column(String(100)) 100 | credits: Mapped[Optional[int]] 101 | 102 | # Связь через ассоциативную модель 103 | student_links: Mapped[List["StudentCourse"]] = relationship(back_populates="course") 104 | 105 | 106 | # ============================================ 107 | # Демонстрация 108 | # ============================================ 109 | 110 | def demo_posts_tags(): 111 | """Демонстрация Posts-Tags""" 112 | print("\n" + "=" * 50) 113 | print("Demo 1: Posts and Tags (Simple M2M) - SQLAlchemy 2.0") 114 | print("=" * 50) 115 | 116 | with Session(engine) as session: 117 | # Создание тегов 118 | tag_python = Tag(name="python") 119 | tag_database = Tag(name="database") 120 | tag_tutorial = Tag(name="tutorial") 121 | 122 | # Создание постов с тегами 123 | post1 = Post( 124 | title="Python Tutorial", 125 | content="Learn Python...", 126 | tags=[tag_python, tag_tutorial] 127 | ) 128 | 129 | post2 = Post( 130 | title="SQLAlchemy Guide", 131 | content="Database ORM...", 132 | tags=[tag_python, tag_database, tag_tutorial] 133 | ) 134 | 135 | session.add_all([post1, post2]) 136 | session.commit() 137 | 138 | # Чтение с selectinload 139 | print("\nPosts and their tags (using selectinload):") 140 | stmt = select(Post).options(selectinload(Post.tags)) 141 | posts = session.execute(stmt).scalars().all() 142 | 143 | for post in posts: 144 | tags_str = ", ".join(tag.name for tag in post.tags) 145 | print(f" '{post.title}': [{tags_str}]") 146 | 147 | # Теги и их посты 148 | print("\nTags and their posts:") 149 | stmt = select(Tag).options(selectinload(Tag.posts)) 150 | tags = session.execute(stmt).scalars().all() 151 | 152 | for tag in tags: 153 | print(f" #{tag.name} - {len(tag.posts)} posts") 154 | 155 | 156 | def demo_students_courses(): 157 | """Демонстрация Students-Courses""" 158 | print("\n" + "=" * 50) 159 | print("Demo 2: Students and Courses (M2M with Extra Data)") 160 | print("=" * 50) 161 | 162 | with Session(engine) as session: 163 | # Создание студентов и курсов 164 | alice = Student(name="Alice Smith", email="alice@university.edu") 165 | bob = Student(name="Bob Johnson", email="bob@university.edu") 166 | 167 | python_course = Course(name="Python Programming", credits=4) 168 | db_course = Course(name="Database Systems", credits=3) 169 | 170 | # Запись на курсы 171 | StudentCourse(student=alice, course=python_course, grade=95, completed=True) 172 | StudentCourse(student=alice, course=db_course, grade=88, completed=True) 173 | StudentCourse(student=bob, course=python_course, grade=92, completed=True) 174 | 175 | session.add_all([alice, bob, python_course, db_course]) 176 | session.commit() 177 | 178 | # Чтение с selectinload 179 | print("\nStudent enrollments (using selectinload):") 180 | stmt = select(Student).options( 181 | selectinload(Student.course_links).selectinload(StudentCourse.course) 182 | ) 183 | students = session.execute(stmt).scalars().all() 184 | 185 | for student in students: 186 | print(f"\n{student.name}:") 187 | for link in student.course_links: 188 | status = f"Grade: {link.grade}" if link.completed else "In Progress" 189 | print(f" - {link.course.name} ({link.course.credits} credits) - {status}") 190 | 191 | 192 | if __name__ == "__main__": 193 | print("=" * 50) 194 | print("SQLAlchemy 2.0 Many-to-Many Relationship") 195 | print("=" * 50) 196 | 197 | Base.metadata.drop_all(engine) 198 | Base.metadata.create_all(engine) 199 | 200 | demo_posts_tags() 201 | demo_students_courses() 202 | 203 | print("\n" + "=" * 50) 204 | print("Completed! Database: many_to_many_v2.db") 205 | print("=" * 50) 206 | -------------------------------------------------------------------------------- /example_05/many_to_many.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 05: Связь Many-to-Many 3 | Демонстрирует реализацию связи многие-ко-многим между таблицами 4 | """ 5 | 6 | from sqlalchemy import create_engine, Column, Integer, String, Table, ForeignKey, DateTime, Boolean 7 | from sqlalchemy.orm import relationship, declarative_base, sessionmaker 8 | from datetime import datetime 9 | 10 | 11 | Base = declarative_base() 12 | engine = create_engine('sqlite:///many_to_many.db', echo=False) 13 | Session = sessionmaker(bind=engine) 14 | 15 | 16 | # ============================================ 17 | # Пример 1: Посты и Теги (простая связь) 18 | # ============================================ 19 | 20 | # Ассоциативная таблица 21 | post_tags = Table( 22 | 'post_tags', 23 | Base.metadata, 24 | Column('post_id', Integer, ForeignKey('posts.id'), primary_key=True), 25 | Column('tag_id', Integer, ForeignKey('tags.id'), primary_key=True) 26 | ) 27 | 28 | 29 | class Post(Base): 30 | """Пост""" 31 | __tablename__ = 'posts' 32 | 33 | id = Column(Integer, primary_key=True) 34 | title = Column(String(200), nullable=False) 35 | content = Column(String) 36 | 37 | tags = relationship("Tag", secondary=post_tags, back_populates="posts") 38 | 39 | 40 | class Tag(Base): 41 | """Тег""" 42 | __tablename__ = 'tags' 43 | 44 | id = Column(Integer, primary_key=True) 45 | name = Column(String(50), unique=True, nullable=False) 46 | 47 | posts = relationship("Post", secondary=post_tags, back_populates="tags") 48 | 49 | 50 | # ============================================ 51 | # Пример 2: Студенты и Курсы (с доп. данными) 52 | # ============================================ 53 | 54 | class StudentCourse(Base): 55 | """Ассоциативная таблица с дополнительными данными""" 56 | __tablename__ = 'student_courses' 57 | 58 | student_id = Column(Integer, ForeignKey('students.id'), primary_key=True) 59 | course_id = Column(Integer, ForeignKey('courses.id'), primary_key=True) 60 | 61 | # Дополнительные данные о записи 62 | enrolled_at = Column(DateTime, default=datetime.utcnow) 63 | grade = Column(Integer) 64 | completed = Column(Boolean, default=False) 65 | 66 | student = relationship("Student", back_populates="course_links") 67 | course = relationship("Course", back_populates="student_links") 68 | 69 | 70 | class Student(Base): 71 | """Студент""" 72 | __tablename__ = 'students' 73 | 74 | id = Column(Integer, primary_key=True) 75 | name = Column(String(100), nullable=False) 76 | email = Column(String(100)) 77 | 78 | course_links = relationship("StudentCourse", back_populates="student") 79 | 80 | 81 | class Course(Base): 82 | """Курс""" 83 | __tablename__ = 'courses' 84 | 85 | id = Column(Integer, primary_key=True) 86 | name = Column(String(100), nullable=False) 87 | credits = Column(Integer) 88 | 89 | student_links = relationship("StudentCourse", back_populates="course") 90 | 91 | 92 | # ============================================ 93 | # Демонстрация 94 | # ============================================ 95 | 96 | def demo_posts_tags(): 97 | """Демонстрация Posts-Tags""" 98 | print("\n" + "=" * 50) 99 | print("Demo 1: Posts and Tags (Simple Many-to-Many)") 100 | print("=" * 50) 101 | 102 | session = Session() 103 | 104 | try: 105 | # Создание тегов 106 | tag_python = Tag(name="python") 107 | tag_database = Tag(name="database") 108 | tag_tutorial = Tag(name="tutorial") 109 | tag_web = Tag(name="web") 110 | 111 | # Создание постов с тегами 112 | post1 = Post( 113 | title="Python Tutorial", 114 | content="Learn Python basics...", 115 | tags=[tag_python, tag_tutorial] 116 | ) 117 | 118 | post2 = Post( 119 | title="SQLAlchemy Guide", 120 | content="Database ORM...", 121 | tags=[tag_python, tag_database, tag_tutorial] 122 | ) 123 | 124 | post3 = Post( 125 | title="Web Development", 126 | content="Build web apps...", 127 | tags=[tag_python, tag_web] 128 | ) 129 | 130 | session.add_all([post1, post2, post3]) 131 | session.commit() 132 | 133 | # Вывод: посты и их теги 134 | print("\nPosts and their tags:") 135 | for post in session.query(Post).all(): 136 | tags_str = ", ".join(tag.name for tag in post.tags) 137 | print(f" '{post.title}': [{tags_str}]") 138 | 139 | # Вывод: теги и их посты 140 | print("\nTags and their posts:") 141 | for tag in session.query(Tag).all(): 142 | print(f" #{tag.name} - {len(tag.posts)} posts") 143 | 144 | finally: 145 | session.close() 146 | 147 | 148 | def demo_students_courses(): 149 | """Демонстрация Students-Courses""" 150 | print("\n" + "=" * 50) 151 | print("Demo 2: Students and Courses (With Extra Data)") 152 | print("=" * 50) 153 | 154 | session = Session() 155 | 156 | try: 157 | # Создание студентов 158 | alice = Student(name="Alice Smith", email="alice@university.edu") 159 | bob = Student(name="Bob Johnson", email="bob@university.edu") 160 | 161 | # Создание курсов 162 | python_course = Course(name="Python Programming", credits=4) 163 | db_course = Course(name="Database Systems", credits=3) 164 | web_course = Course(name="Web Development", credits=4) 165 | 166 | # Запись студентов на курсы с оценками 167 | enrollment1 = StudentCourse(student=alice, course=python_course, grade=95, completed=True) 168 | enrollment2 = StudentCourse(student=alice, course=db_course, grade=88, completed=True) 169 | enrollment3 = StudentCourse(student=bob, course=python_course, grade=92, completed=True) 170 | enrollment4 = StudentCourse(student=bob, course=web_course, completed=False) 171 | 172 | session.add_all([alice, bob, python_course, db_course, web_course]) 173 | session.commit() 174 | 175 | # Вывод информации о студентах 176 | print("\nStudent enrollments:") 177 | for student in session.query(Student).all(): 178 | print(f"\n{student.name}:") 179 | for link in student.course_links: 180 | status = f"Grade: {link.grade}" if link.completed else "In Progress" 181 | print(f" - {link.course.name} ({link.course.credits} credits) - {status}") 182 | 183 | # Вывод информации о курсах 184 | print("\nCourse enrollments:") 185 | for course in session.query(Course).all(): 186 | print(f"\n{course.name}:") 187 | print(f" Enrolled: {len(course.student_links)} students") 188 | for link in course.student_links: 189 | status = f"{link.grade}%" if link.completed else "In Progress" 190 | print(f" - {link.student.name}: {status}") 191 | 192 | finally: 193 | session.close() 194 | 195 | 196 | if __name__ == "__main__": 197 | print("=" * 50) 198 | print("SQLAlchemy Many-to-Many Relationship") 199 | print("=" * 50) 200 | 201 | Base.metadata.drop_all(engine) 202 | Base.metadata.create_all(engine) 203 | 204 | demo_posts_tags() 205 | demo_students_courses() 206 | 207 | print("\n" + "=" * 50) 208 | print("Completed! Database: many_to_many.db") 209 | print("=" * 50) 210 | -------------------------------------------------------------------------------- /example_04/one_to_many.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 04: Связь One-to-Many (1-Many) 3 | Демонстрирует реализацию связи один-ко-многим между таблицами 4 | """ 5 | 6 | from sqlalchemy import create_engine, Column, Integer, String, Text, ForeignKey, DateTime, Boolean 7 | from sqlalchemy.orm import relationship, declarative_base, sessionmaker 8 | from datetime import datetime 9 | 10 | 11 | Base = declarative_base() 12 | engine = create_engine('sqlite:///one_to_many.db', echo=False) 13 | Session = sessionmaker(bind=engine) 14 | 15 | 16 | # ============================================ 17 | # Пример 1: Автор и Книги 18 | # ============================================ 19 | 20 | class Author(Base): 21 | """Автор""" 22 | __tablename__ = 'authors' 23 | 24 | id = Column(Integer, primary_key=True) 25 | name = Column(String(100), nullable=False) 26 | country = Column(String(50)) 27 | 28 | # One-to-Many: один автор - много книг 29 | books = relationship("Book", back_populates="author", cascade="all, delete-orphan") 30 | 31 | def __repr__(self): 32 | return f"" 33 | 34 | 35 | class Book(Base): 36 | """Книга""" 37 | __tablename__ = 'books' 38 | 39 | id = Column(Integer, primary_key=True) 40 | title = Column(String(200), nullable=False) 41 | isbn = Column(String(20), unique=True) 42 | published_year = Column(Integer) 43 | 44 | # Many-to-One: много книг - один автор 45 | author_id = Column(Integer, ForeignKey('authors.id')) 46 | author = relationship("Author", back_populates="books") 47 | 48 | def __repr__(self): 49 | return f"" 50 | 51 | 52 | # ============================================ 53 | # Пример 2: Блог (User и Posts) 54 | # ============================================ 55 | 56 | class BlogUser(Base): 57 | """Пользователь блога""" 58 | __tablename__ = 'blog_users' 59 | 60 | id = Column(Integer, primary_key=True) 61 | username = Column(String(50), unique=True, nullable=False) 62 | email = Column(String(100), unique=True) 63 | 64 | posts = relationship("BlogPost", back_populates="author", cascade="all, delete-orphan") 65 | comments = relationship("Comment", back_populates="author", cascade="all, delete-orphan") 66 | 67 | 68 | class BlogPost(Base): 69 | """Пост в блоге""" 70 | __tablename__ = 'blog_posts' 71 | 72 | id = Column(Integer, primary_key=True) 73 | title = Column(String(200), nullable=False) 74 | content = Column(Text) 75 | created_at = Column(DateTime, default=datetime.utcnow) 76 | is_published = Column(Boolean, default=False) 77 | 78 | author_id = Column(Integer, ForeignKey('blog_users.id')) 79 | author = relationship("BlogUser", back_populates="posts") 80 | 81 | # Один пост может иметь много комментариев 82 | comments = relationship("Comment", back_populates="post", cascade="all, delete-orphan") 83 | 84 | 85 | class Comment(Base): 86 | """Комментарий""" 87 | __tablename__ = 'comments' 88 | 89 | id = Column(Integer, primary_key=True) 90 | content = Column(Text, nullable=False) 91 | created_at = Column(DateTime, default=datetime.utcnow) 92 | 93 | author_id = Column(Integer, ForeignKey('blog_users.id')) 94 | author = relationship("BlogUser", back_populates="comments") 95 | 96 | post_id = Column(Integer, ForeignKey('blog_posts.id')) 97 | post = relationship("BlogPost", back_populates="comments") 98 | 99 | 100 | # ============================================ 101 | # Демонстрация 102 | # ============================================ 103 | 104 | def demo_author_books(): 105 | """Демонстрация Author-Books""" 106 | print("\n" + "=" * 50) 107 | print("Demo 1: Authors and Books") 108 | print("=" * 50) 109 | 110 | session = Session() 111 | 112 | try: 113 | # Создание автора с книгами 114 | author = Author( 115 | name="George Orwell", 116 | country="UK", 117 | books=[ 118 | Book(title="1984", isbn="978-0-452-28423-4", published_year=1949), 119 | Book(title="Animal Farm", isbn="978-0-452-28424-1", published_year=1945), 120 | ] 121 | ) 122 | 123 | session.add(author) 124 | session.commit() 125 | 126 | # Вывод 127 | print(f"\nAuthor: {author.name}") 128 | print(f"Books ({len(author.books)}):") 129 | for book in author.books: 130 | print(f" - {book.title} ({book.published_year})") 131 | 132 | # Добавление новой книги 133 | new_book = Book(title="Homage to Catalonia", published_year=1938) 134 | author.books.append(new_book) 135 | session.commit() 136 | 137 | print(f"\nAfter adding new book: {len(author.books)} books") 138 | 139 | finally: 140 | session.close() 141 | 142 | 143 | def demo_blog(): 144 | """Демонстрация блога""" 145 | print("\n" + "=" * 50) 146 | print("Demo 2: Blog (Users, Posts, Comments)") 147 | print("=" * 50) 148 | 149 | session = Session() 150 | 151 | try: 152 | # Создание пользователей 153 | user1 = BlogUser(username="alice", email="alice@blog.com") 154 | user2 = BlogUser(username="bob", email="bob@blog.com") 155 | 156 | # Создание постов 157 | post1 = BlogPost( 158 | title="Introduction to Python", 159 | content="Python is a great language...", 160 | author=user1, 161 | is_published=True 162 | ) 163 | 164 | post2 = BlogPost( 165 | title="SQLAlchemy Tutorial", 166 | content="Let's learn SQLAlchemy...", 167 | author=user1, 168 | is_published=True 169 | ) 170 | 171 | # Комментарии 172 | comment1 = Comment(content="Great post!", author=user2, post=post1) 173 | comment2 = Comment(content="Very helpful!", author=user2, post=post1) 174 | comment3 = Comment(content="Thanks!", author=user1, post=post1) 175 | 176 | session.add_all([user1, user2]) 177 | session.commit() 178 | 179 | # Вывод статистики 180 | print(f"\n{user1.username} has {len(user1.posts)} posts") 181 | for post in user1.posts: 182 | print(f" '{post.title}' - {len(post.comments)} comments") 183 | 184 | print(f"\n{user2.username} has {len(user2.comments)} comments") 185 | 186 | finally: 187 | session.close() 188 | 189 | 190 | def demo_cascade_delete(): 191 | """Демонстрация каскадного удаления""" 192 | print("\n" + "=" * 50) 193 | print("Demo 3: Cascade Delete") 194 | print("=" * 50) 195 | 196 | session = Session() 197 | 198 | try: 199 | # Создание автора с книгами 200 | author = Author( 201 | name="Test Author", 202 | books=[ 203 | Book(title="Book 1"), 204 | Book(title="Book 2"), 205 | Book(title="Book 3"), 206 | ] 207 | ) 208 | session.add(author) 209 | session.commit() 210 | 211 | author_id = author.id 212 | book_count_before = session.query(Book).filter_by(author_id=author_id).count() 213 | 214 | print(f"\nAuthor created with {book_count_before} books") 215 | 216 | # Удаление автора (книги удалятся автоматически) 217 | session.delete(author) 218 | session.commit() 219 | 220 | book_count_after = session.query(Book).filter_by(author_id=author_id).count() 221 | print(f"After deleting author: {book_count_after} books remain") 222 | print("✓ Cascade delete worked!") 223 | 224 | finally: 225 | session.close() 226 | 227 | 228 | if __name__ == "__main__": 229 | print("=" * 50) 230 | print("SQLAlchemy One-to-Many Relationship") 231 | print("=" * 50) 232 | 233 | Base.metadata.drop_all(engine) 234 | Base.metadata.create_all(engine) 235 | 236 | demo_author_books() 237 | demo_blog() 238 | demo_cascade_delete() 239 | 240 | print("\n" + "=" * 50) 241 | print("Completed! Database: one_to_many.db") 242 | print("=" * 50) 243 | -------------------------------------------------------------------------------- /example_08/test_async.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример тестирования асинхронного кода SQLAlchemy 3 | Требует: pip install pytest pytest-asyncio 4 | """ 5 | 6 | import pytest 7 | from typing import AsyncGenerator 8 | from datetime import datetime, timezone 9 | 10 | from sqlalchemy import String, select 11 | from sqlalchemy.ext.asyncio import ( 12 | create_async_engine, 13 | async_sessionmaker, 14 | AsyncSession, 15 | ) 16 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 17 | 18 | 19 | # ============================================ 20 | # Модели для тестирования 21 | # ============================================ 22 | 23 | class Base(DeclarativeBase): 24 | pass 25 | 26 | 27 | class User(Base): 28 | __tablename__ = 'test_users' 29 | 30 | id: Mapped[int] = mapped_column(primary_key=True) 31 | username: Mapped[str] = mapped_column(String(50), unique=True) 32 | email: Mapped[str] = mapped_column(String(100)) 33 | created_at: Mapped[datetime] = mapped_column( 34 | default=lambda: datetime.now(timezone.utc) 35 | ) 36 | 37 | 38 | # ============================================ 39 | # Fixtures 40 | # ============================================ 41 | 42 | @pytest.fixture(scope="function") 43 | async def async_engine(): 44 | """Создание тестового async engine""" 45 | engine = create_async_engine( 46 | 'sqlite+aiosqlite:///:memory:', # In-memory для тестов 47 | echo=False 48 | ) 49 | 50 | # Создание таблиц 51 | async with engine.begin() as conn: 52 | await conn.run_sync(Base.metadata.create_all) 53 | 54 | yield engine 55 | 56 | # Очистка 57 | await engine.dispose() 58 | 59 | 60 | @pytest.fixture(scope="function") 61 | async def async_session(async_engine) -> AsyncGenerator[AsyncSession, None]: 62 | """Создание тестовой async session""" 63 | async_session_maker = async_sessionmaker( 64 | async_engine, 65 | class_=AsyncSession, 66 | expire_on_commit=False 67 | ) 68 | 69 | async with async_session_maker() as session: 70 | yield session 71 | 72 | 73 | # ============================================ 74 | # Тесты CREATE 75 | # ============================================ 76 | 77 | @pytest.mark.asyncio 78 | async def test_create_user(async_session: AsyncSession): 79 | """Тест создания пользователя""" 80 | # Arrange 81 | user = User(username="testuser", email="test@example.com") 82 | 83 | # Act 84 | async_session.add(user) 85 | await async_session.commit() 86 | 87 | # Assert 88 | assert user.id is not None 89 | assert user.username == "testuser" 90 | assert user.created_at is not None 91 | 92 | 93 | @pytest.mark.asyncio 94 | async def test_create_multiple_users(async_session: AsyncSession): 95 | """Тест создания нескольких пользователей""" 96 | # Arrange 97 | users = [ 98 | User(username=f"user{i}", email=f"user{i}@example.com") 99 | for i in range(5) 100 | ] 101 | 102 | # Act 103 | async_session.add_all(users) 104 | await async_session.commit() 105 | 106 | # Assert 107 | stmt = select(User) 108 | result = await async_session.execute(stmt) 109 | all_users = result.scalars().all() 110 | assert len(all_users) == 5 111 | 112 | 113 | # ============================================ 114 | # Тесты READ 115 | # ============================================ 116 | 117 | @pytest.mark.asyncio 118 | async def test_read_user_by_id(async_session: AsyncSession): 119 | """Тест чтения пользователя по ID""" 120 | # Arrange 121 | user = User(username="alice", email="alice@example.com") 122 | async_session.add(user) 123 | await async_session.commit() 124 | user_id = user.id 125 | 126 | # Act 127 | found_user = await async_session.get(User, user_id) 128 | 129 | # Assert 130 | assert found_user is not None 131 | assert found_user.username == "alice" 132 | 133 | 134 | @pytest.mark.asyncio 135 | async def test_read_user_by_username(async_session: AsyncSession): 136 | """Тест поиска пользователя по username""" 137 | # Arrange 138 | user = User(username="bob", email="bob@example.com") 139 | async_session.add(user) 140 | await async_session.commit() 141 | 142 | # Act 143 | stmt = select(User).where(User.username == "bob") 144 | result = await async_session.execute(stmt) 145 | found_user = result.scalar_one() 146 | 147 | # Assert 148 | assert found_user.username == "bob" 149 | assert found_user.email == "bob@example.com" 150 | 151 | 152 | @pytest.mark.asyncio 153 | async def test_read_all_users(async_session: AsyncSession): 154 | """Тест получения всех пользователей""" 155 | # Arrange 156 | users = [ 157 | User(username="user1", email="user1@example.com"), 158 | User(username="user2", email="user2@example.com"), 159 | User(username="user3", email="user3@example.com"), 160 | ] 161 | async_session.add_all(users) 162 | await async_session.commit() 163 | 164 | # Act 165 | stmt = select(User) 166 | result = await async_session.execute(stmt) 167 | all_users = result.scalars().all() 168 | 169 | # Assert 170 | assert len(all_users) == 3 171 | usernames = [u.username for u in all_users] 172 | assert "user1" in usernames 173 | assert "user2" in usernames 174 | assert "user3" in usernames 175 | 176 | 177 | @pytest.mark.asyncio 178 | async def test_read_user_not_found(async_session: AsyncSession): 179 | """Тест когда пользователь не найден""" 180 | # Act 181 | user = await async_session.get(User, 999) 182 | 183 | # Assert 184 | assert user is None 185 | 186 | 187 | # ============================================ 188 | # Тесты UPDATE 189 | # ============================================ 190 | 191 | @pytest.mark.asyncio 192 | async def test_update_user(async_session: AsyncSession): 193 | """Тест обновления пользователя""" 194 | # Arrange 195 | user = User(username="charlie", email="charlie@example.com") 196 | async_session.add(user) 197 | await async_session.commit() 198 | 199 | # Act 200 | user.email = "charlie_new@example.com" 201 | await async_session.commit() 202 | 203 | # Assert 204 | stmt = select(User).where(User.username == "charlie") 205 | result = await async_session.execute(stmt) 206 | updated_user = result.scalar_one() 207 | assert updated_user.email == "charlie_new@example.com" 208 | 209 | 210 | # ============================================ 211 | # Тесты DELETE 212 | # ============================================ 213 | 214 | @pytest.mark.asyncio 215 | async def test_delete_user(async_session: AsyncSession): 216 | """Тест удаления пользователя""" 217 | # Arrange 218 | user = User(username="temp", email="temp@example.com") 219 | async_session.add(user) 220 | await async_session.commit() 221 | user_id = user.id 222 | 223 | # Act 224 | await async_session.delete(user) 225 | await async_session.commit() 226 | 227 | # Assert 228 | deleted_user = await async_session.get(User, user_id) 229 | assert deleted_user is None 230 | 231 | 232 | # ============================================ 233 | # Тесты сложных запросов 234 | # ============================================ 235 | 236 | @pytest.mark.asyncio 237 | async def test_filter_users(async_session: AsyncSession): 238 | """Тест фильтрации пользователей""" 239 | # Arrange 240 | users = [ 241 | User(username="alice", email="alice@example.com"), 242 | User(username="bob", email="bob@gmail.com"), 243 | User(username="charlie", email="charlie@gmail.com"), 244 | ] 245 | async_session.add_all(users) 246 | await async_session.commit() 247 | 248 | # Act 249 | stmt = select(User).where(User.email.like("%gmail.com")) 250 | result = await async_session.execute(stmt) 251 | gmail_users = result.scalars().all() 252 | 253 | # Assert 254 | assert len(gmail_users) == 2 255 | usernames = [u.username for u in gmail_users] 256 | assert "bob" in usernames 257 | assert "charlie" in usernames 258 | 259 | 260 | @pytest.mark.asyncio 261 | async def test_count_users(async_session: AsyncSession): 262 | """Тест подсчета пользователей""" 263 | # Arrange 264 | users = [ 265 | User(username=f"user{i}", email=f"user{i}@example.com") 266 | for i in range(10) 267 | ] 268 | async_session.add_all(users) 269 | await async_session.commit() 270 | 271 | # Act 272 | from sqlalchemy import func 273 | stmt = select(func.count()).select_from(User) 274 | result = await async_session.execute(stmt) 275 | count = result.scalar() 276 | 277 | # Assert 278 | assert count == 10 279 | 280 | 281 | # ============================================ 282 | # Тесты уникальности 283 | # ============================================ 284 | 285 | @pytest.mark.asyncio 286 | async def test_unique_username_constraint(async_session: AsyncSession): 287 | """Тест уникальности username""" 288 | # Arrange 289 | user1 = User(username="unique", email="user1@example.com") 290 | async_session.add(user1) 291 | await async_session.commit() 292 | 293 | # Act & Assert 294 | user2 = User(username="unique", email="user2@example.com") 295 | async_session.add(user2) 296 | 297 | with pytest.raises(Exception): # IntegrityError 298 | await async_session.commit() 299 | 300 | 301 | # ============================================ 302 | # Запуск тестов 303 | # ============================================ 304 | 305 | if __name__ == "__main__": 306 | """ 307 | Запуск тестов: 308 | 309 | # Установка зависимостей 310 | pip install pytest pytest-asyncio 311 | 312 | # Запуск всех тестов 313 | pytest test_async.py -v 314 | 315 | # Запуск конкретного теста 316 | pytest test_async.py::test_create_user -v 317 | 318 | # С выводом print 319 | pytest test_async.py -v -s 320 | 321 | # С покрытием 322 | pip install pytest-cov 323 | pytest test_async.py --cov=. --cov-report=html 324 | """ 325 | pytest.main([__file__, "-v", "-s"]) 326 | -------------------------------------------------------------------------------- /example_08/async_sqlite.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 08: Асинхронные запросы с SQLAlchemy 2.0 + aiosqlite 3 | Демонстрирует использование async/await для работы с базой данных 4 | """ 5 | 6 | import asyncio 7 | from typing import Optional, List 8 | from datetime import datetime, timezone 9 | 10 | from sqlalchemy import String, select, update, delete, func 11 | from sqlalchemy.ext.asyncio import ( 12 | create_async_engine, 13 | async_sessionmaker, 14 | AsyncSession, 15 | ) 16 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, selectinload 17 | 18 | 19 | # ============================================ 20 | # SQLAlchemy 2.0 Async Base 21 | # ============================================ 22 | 23 | class Base(DeclarativeBase): 24 | pass 25 | 26 | 27 | # Создание async engine для SQLite 28 | async_engine = create_async_engine( 29 | 'sqlite+aiosqlite:///async_example.db', 30 | echo=True # Показывать SQL запросы 31 | ) 32 | 33 | # Фабрика для создания async сессий 34 | AsyncSessionLocal = async_sessionmaker( 35 | async_engine, 36 | class_=AsyncSession, 37 | expire_on_commit=False 38 | ) 39 | 40 | 41 | # ============================================ 42 | # Модели 43 | # ============================================ 44 | 45 | class User(Base): 46 | """Модель пользователя""" 47 | __tablename__ = 'users' 48 | 49 | id: Mapped[int] = mapped_column(primary_key=True) 50 | username: Mapped[str] = mapped_column(String(50), unique=True) 51 | email: Mapped[Optional[str]] = mapped_column(String(100)) 52 | age: Mapped[Optional[int]] 53 | created_at: Mapped[datetime] = mapped_column( 54 | default=lambda: datetime.now(timezone.utc) 55 | ) 56 | 57 | # Relationships 58 | posts: Mapped[List["Post"]] = relationship( 59 | back_populates="author", 60 | cascade="all, delete-orphan" 61 | ) 62 | 63 | 64 | class Post(Base): 65 | """Модель поста""" 66 | __tablename__ = 'posts' 67 | 68 | id: Mapped[int] = mapped_column(primary_key=True) 69 | title: Mapped[str] = mapped_column(String(200)) 70 | content: Mapped[Optional[str]] 71 | author_id: Mapped[int] = mapped_column() 72 | created_at: Mapped[datetime] = mapped_column( 73 | default=lambda: datetime.now(timezone.utc) 74 | ) 75 | 76 | # Relationships 77 | author: Mapped["User"] = relationship(back_populates="posts") 78 | 79 | 80 | # ============================================ 81 | # CREATE - Создание записей 82 | # ============================================ 83 | 84 | async def create_users(): 85 | """Асинхронное создание пользователей""" 86 | print("\n" + "=" * 50) 87 | print("CREATE Operations (Async)") 88 | print("=" * 50) 89 | 90 | async with AsyncSessionLocal() as session: 91 | # Создание нескольких пользователей 92 | users = [ 93 | User(username="alice", email="alice@example.com", age=25), 94 | User(username="bob", email="bob@example.com", age=30), 95 | User(username="charlie", email="charlie@example.com", age=28), 96 | ] 97 | 98 | session.add_all(users) 99 | await session.commit() 100 | 101 | print(f"✓ Created {len(users)} users") 102 | 103 | # Создание постов для пользователей 104 | async with AsyncSessionLocal() as session: 105 | # Загружаем пользователя 106 | stmt = select(User).where(User.username == "alice") 107 | result = await session.execute(stmt) 108 | alice = result.scalar_one() 109 | 110 | # Создаем посты 111 | posts = [ 112 | Post(title="My First Post", content="Hello World!", author_id=alice.id), 113 | Post(title="SQLAlchemy Async", content="Async is awesome!", author_id=alice.id), 114 | ] 115 | 116 | session.add_all(posts) 117 | await session.commit() 118 | 119 | print(f"✓ Created {len(posts)} posts for {alice.username}") 120 | 121 | 122 | # ============================================ 123 | # READ - Чтение записей 124 | # ============================================ 125 | 126 | async def read_users(): 127 | """Асинхронное чтение пользователей""" 128 | print("\n" + "=" * 50) 129 | print("READ Operations (Async)") 130 | print("=" * 50) 131 | 132 | async with AsyncSessionLocal() as session: 133 | # 1. Все пользователи 134 | stmt = select(User) 135 | result = await session.execute(stmt) 136 | users = result.scalars().all() 137 | 138 | print(f"\n1. All users ({len(users)}):") 139 | for user in users: 140 | print(f" {user.username} - {user.email} (age: {user.age})") 141 | 142 | # 2. Пользователь по ID 143 | user = await session.get(User, 1) 144 | print(f"\n2. User by ID(1): {user.username if user else 'Not found'}") 145 | 146 | # 3. С фильтром 147 | stmt = select(User).where(User.age >= 28) 148 | result = await session.execute(stmt) 149 | users = result.scalars().all() 150 | print(f"\n3. Users with age >= 28: {[u.username for u in users]}") 151 | 152 | # 4. Подсчет 153 | stmt = select(func.count()).select_from(User) 154 | result = await session.execute(stmt) 155 | count = result.scalar() 156 | print(f"\n4. Total users count: {count}") 157 | 158 | # 5. С сортировкой и лимитом 159 | stmt = select(User).order_by(User.username).limit(2) 160 | result = await session.execute(stmt) 161 | users = result.scalars().all() 162 | print(f"\n5. First 2 users (sorted): {[u.username for u in users]}") 163 | 164 | 165 | # ============================================ 166 | # READ with Relationships 167 | # ============================================ 168 | 169 | async def read_with_relationships(): 170 | """Чтение с загрузкой связанных объектов""" 171 | print("\n" + "=" * 50) 172 | print("READ with Relationships (Async)") 173 | print("=" * 50) 174 | 175 | async with AsyncSessionLocal() as session: 176 | # Загрузка пользователя с постами (selectinload) 177 | stmt = select(User).options(selectinload(User.posts)).where(User.username == "alice") 178 | result = await session.execute(stmt) 179 | alice = result.scalar_one() 180 | 181 | print(f"\nUser: {alice.username}") 182 | print(f"Posts ({len(alice.posts)}):") 183 | for post in alice.posts: 184 | print(f" - {post.title}: {post.content}") 185 | 186 | 187 | # ============================================ 188 | # UPDATE - Обновление записей 189 | # ============================================ 190 | 191 | async def update_users(): 192 | """Асинхронное обновление пользователей""" 193 | print("\n" + "=" * 50) 194 | print("UPDATE Operations (Async)") 195 | print("=" * 50) 196 | 197 | async with AsyncSessionLocal() as session: 198 | # 1. Обновление через объект 199 | stmt = select(User).where(User.username == "bob") 200 | result = await session.execute(stmt) 201 | bob = result.scalar_one() 202 | 203 | old_email = bob.email 204 | bob.email = "bob_new@example.com" 205 | await session.commit() 206 | 207 | print(f"✓ Updated {bob.username} email: {old_email} -> {bob.email}") 208 | 209 | async with AsyncSessionLocal() as session: 210 | # 2. Массовое обновление 211 | stmt = update(User).where(User.age < 30).values(age=User.age + 1) 212 | result = await session.execute(stmt) 213 | await session.commit() 214 | 215 | print(f"✓ Updated {result.rowcount} users (age + 1)") 216 | 217 | 218 | # ============================================ 219 | # DELETE - Удаление записей 220 | # ============================================ 221 | 222 | async def delete_users(): 223 | """Асинхронное удаление пользователей""" 224 | print("\n" + "=" * 50) 225 | print("DELETE Operations (Async)") 226 | print("=" * 50) 227 | 228 | async with AsyncSessionLocal() as session: 229 | # 1. Удаление через объект 230 | stmt = select(User).where(User.username == "charlie") 231 | result = await session.execute(stmt) 232 | charlie = result.scalar_one() 233 | 234 | await session.delete(charlie) 235 | await session.commit() 236 | 237 | print(f"✓ Deleted user: charlie") 238 | 239 | async with AsyncSessionLocal() as session: 240 | # 2. Массовое удаление 241 | stmt = delete(User).where(User.age > 100) 242 | result = await session.execute(stmt) 243 | await session.commit() 244 | 245 | print(f"✓ Deleted {result.rowcount} users with age > 100") 246 | 247 | 248 | # ============================================ 249 | # Advanced Queries 250 | # ============================================ 251 | 252 | async def advanced_queries(): 253 | """Сложные асинхронные запросы""" 254 | print("\n" + "=" * 50) 255 | print("Advanced Async Queries") 256 | print("=" * 50) 257 | 258 | async with AsyncSessionLocal() as session: 259 | # 1. Агрегация 260 | stmt = select( 261 | User.username, 262 | func.count(Post.id).label('post_count') 263 | ).outerjoin(Post).group_by(User.username) 264 | 265 | result = await session.execute(stmt) 266 | stats = result.all() 267 | 268 | print("\nPost statistics by user:") 269 | for username, post_count in stats: 270 | print(f" {username}: {post_count} posts") 271 | 272 | # 2. Фильтрация с JOIN 273 | stmt = select(User).join(Post).where(Post.title.like("%Async%")) 274 | result = await session.execute(stmt) 275 | users = result.scalars().all() 276 | 277 | print(f"\nUsers with 'Async' in post titles: {[u.username for u in users]}") 278 | 279 | 280 | # ============================================ 281 | # Конкурентные операции 282 | # ============================================ 283 | 284 | async def concurrent_operations(): 285 | """Демонстрация конкурентных операций""" 286 | print("\n" + "=" * 50) 287 | print("Concurrent Operations") 288 | print("=" * 50) 289 | 290 | async def get_user(username: str) -> Optional[User]: 291 | """Получить пользователя по имени""" 292 | async with AsyncSessionLocal() as session: 293 | stmt = select(User).where(User.username == username) 294 | result = await session.execute(stmt) 295 | return result.scalar_one_or_none() 296 | 297 | # Запускаем несколько запросов параллельно 298 | users = await asyncio.gather( 299 | get_user("alice"), 300 | get_user("bob"), 301 | get_user("charlie"), 302 | ) 303 | 304 | print("\nConcurrent user lookups:") 305 | for user in users: 306 | if user: 307 | print(f" Found: {user.username}") 308 | else: 309 | print(f" Not found") 310 | 311 | 312 | # ============================================ 313 | # Main 314 | # ============================================ 315 | 316 | async def init_db(): 317 | """Инициализация базы данных""" 318 | async with async_engine.begin() as conn: 319 | await conn.run_sync(Base.metadata.drop_all) 320 | await conn.run_sync(Base.metadata.create_all) 321 | 322 | 323 | async def main(): 324 | """Главная функция""" 325 | print("=" * 50) 326 | print("SQLAlchemy 2.0 Async with aiosqlite") 327 | print("=" * 50) 328 | 329 | # Инициализация БД 330 | await init_db() 331 | 332 | # Операции 333 | await create_users() 334 | await read_users() 335 | await read_with_relationships() 336 | await update_users() 337 | await advanced_queries() 338 | await concurrent_operations() 339 | await delete_users() 340 | 341 | print("\n" + "=" * 50) 342 | print("Completed! Database: async_example.db") 343 | print("=" * 50) 344 | 345 | # Закрытие engine 346 | await async_engine.dispose() 347 | 348 | 349 | if __name__ == "__main__": 350 | asyncio.run(main()) 351 | -------------------------------------------------------------------------------- /example_08/README.md: -------------------------------------------------------------------------------- 1 | # Пример 08: Асинхронные запросы в SQLAlchemy 2.0 2 | 3 | ## Обзор 4 | 5 | Этот пример демонстрирует использование асинхронных операций с SQLAlchemy 2.0. Асинхронное программирование позволяет эффективно обрабатывать множество запросов одновременно без блокировки. 6 | 7 | ## ER-диаграмма (async_postgres.py) 8 | 9 | ```mermaid 10 | erDiagram 11 | USER ||--o{ POST : "writes" 12 | USER ||--o{ COMMENT : "writes" 13 | POST ||--o{ COMMENT : "has" 14 | 15 | USER { 16 | int id PK 17 | string username UK 18 | string email UK 19 | int age 20 | boolean is_active 21 | decimal balance 22 | datetime created_at 23 | datetime updated_at 24 | } 25 | 26 | POST { 27 | int id PK 28 | string title 29 | text content 30 | int author_id FK "references users(id)" 31 | int views 32 | boolean published 33 | datetime created_at 34 | } 35 | 36 | COMMENT { 37 | int id PK 38 | text content 39 | int post_id FK "references posts(id)" 40 | int author_id FK "references users(id)" 41 | datetime created_at 42 | } 43 | ``` 44 | 45 | **Особенности async моделей:** 46 | - Те же модели, что и в sync версии 47 | - Используются с `AsyncSession` вместо `Session` 48 | - Требуют `await` для всех операций с БД 49 | - Relationships загружаются через `selectinload()` или `AsyncAttrs` 50 | 51 | ## Файлы 52 | 53 | - **async_sqlite.py** - Пример с aiosqlite (для SQLite) 54 | - **async_postgres.py** - Пример с asyncpg (для PostgreSQL) 55 | 56 | ## Зачем нужен async? 57 | 58 | ### Преимущества 59 | 60 | 1. **Высокая производительность** - обработка множества запросов параллельно 61 | 2. **Эффективное использование ресурсов** - не блокирует поток при ожидании БД 62 | 3. **Масштабируемость** - может обслуживать тысячи одновременных соединений 63 | 4. **Современные фреймворки** - FastAPI, aiohttp требуют async 64 | 65 | ### Когда использовать 66 | 67 | ✅ **Используйте async когда:** 68 | - Веб-приложения с высокой нагрузкой (FastAPI, aiohttp) 69 | - Много параллельных запросов к БД 70 | - Микросервисы 71 | - Real-time приложения 72 | 73 | ❌ **Не используйте async когда:** 74 | - Простые скрипты 75 | - Низкая нагрузка 76 | - Команда не знакома с async/await 77 | - Сложность не оправдана 78 | 79 | ## Ключевые отличия от синхронного кода 80 | 81 | ### 1. Engine 82 | 83 | ```python 84 | # Синхронный 85 | from sqlalchemy import create_engine 86 | engine = create_engine('sqlite:///database.db') 87 | 88 | # Асинхронный 89 | from sqlalchemy.ext.asyncio import create_async_engine 90 | async_engine = create_async_engine('sqlite+aiosqlite:///database.db') 91 | ``` 92 | 93 | ### 2. Session 94 | 95 | ```python 96 | # Синхронный 97 | from sqlalchemy.orm import Session 98 | with Session(engine) as session: 99 | user = session.get(User, 1) 100 | 101 | # Асинхронный 102 | from sqlalchemy.ext.asyncio import AsyncSession 103 | async with AsyncSession(async_engine) as session: 104 | user = await session.get(User, 1) 105 | ``` 106 | 107 | ### 3. Запросы 108 | 109 | ```python 110 | # Синхронный 111 | users = session.execute(select(User)).scalars().all() 112 | 113 | # Асинхронный 114 | users = (await session.execute(select(User))).scalars().all() 115 | ``` 116 | 117 | ### 4. Relationships 118 | 119 | ```python 120 | # Синхронный 121 | user.posts # Загружаются автоматически 122 | 123 | # Асинхронный (опция 1: selectinload) 124 | stmt = select(User).options(selectinload(User.posts)) 125 | user = (await session.execute(stmt)).scalar_one() 126 | user.posts # Уже загружены 127 | 128 | # Асинхронный (опция 2: AsyncAttrs) 129 | class Base(AsyncAttrs, DeclarativeBase): 130 | pass 131 | 132 | posts = await user.awaitable_attrs.posts 133 | ``` 134 | 135 | ## Database URLs для async 136 | 137 | ### SQLite + aiosqlite 138 | ```python 139 | DATABASE_URL = "sqlite+aiosqlite:///database.db" 140 | # Требует: pip install aiosqlite 141 | ``` 142 | 143 | ### PostgreSQL + asyncpg 144 | ```python 145 | DATABASE_URL = "postgresql+asyncpg://user:password@localhost/dbname" 146 | # Требует: pip install asyncpg 147 | ``` 148 | 149 | ### MySQL + aiomysql 150 | ```python 151 | DATABASE_URL = "mysql+aiomysql://user:password@localhost/dbname" 152 | # Требует: pip install aiomysql 153 | ``` 154 | 155 | ## Основные паттерны 156 | 157 | ### 1. Async Context Manager 158 | 159 | ```python 160 | async with AsyncSessionLocal() as session: 161 | # Работа с БД 162 | result = await session.execute(select(User)) 163 | users = result.scalars().all() 164 | # Автоматическое закрытие сессии 165 | ``` 166 | 167 | ### 2. Eager Loading (selectinload) 168 | 169 | ```python 170 | # Предотвращение N+1 проблемы 171 | stmt = ( 172 | select(User) 173 | .options(selectinload(User.posts)) 174 | .where(User.username == "alice") 175 | ) 176 | 177 | result = await session.execute(stmt) 178 | user = result.scalar_one() 179 | 180 | # posts уже загружены, нет дополнительных запросов 181 | for post in user.posts: 182 | print(post.title) 183 | ``` 184 | 185 | ### 3. Вложенная загрузка 186 | 187 | ```python 188 | # Загрузка пользователя -> постов -> комментариев 189 | stmt = ( 190 | select(User) 191 | .options( 192 | selectinload(User.posts).selectinload(Post.comments) 193 | ) 194 | ) 195 | 196 | result = await session.execute(stmt) 197 | users = result.scalars().all() 198 | ``` 199 | 200 | ### 4. Транзакции 201 | 202 | ```python 203 | async with AsyncSessionLocal() as session: 204 | async with session.begin(): 205 | # Все операции в одной транзакции 206 | user = User(username="alice") 207 | session.add(user) 208 | # Автоматический commit или rollback 209 | ``` 210 | 211 | ### 5. Конкурентные запросы 212 | 213 | ```python 214 | import asyncio 215 | 216 | async def get_user(session, user_id): 217 | return await session.get(User, user_id) 218 | 219 | async with AsyncSessionLocal() as session: 220 | # Запускаем параллельно 221 | results = await asyncio.gather( 222 | get_user(session, 1), 223 | get_user(session, 2), 224 | get_user(session, 3), 225 | ) 226 | ``` 227 | 228 | ### 6. Streaming для больших результатов 229 | 230 | ```python 231 | async with AsyncSessionLocal() as session: 232 | result = await session.stream(select(User)) 233 | 234 | # Обрабатываем по одному 235 | async for (user,) in result: 236 | process(user) 237 | ``` 238 | 239 | ## Инициализация базы данных 240 | 241 | ```python 242 | async def init_db(): 243 | """Создание таблиц асинхронно""" 244 | async with async_engine.begin() as conn: 245 | await conn.run_sync(Base.metadata.drop_all) 246 | await conn.run_sync(Base.metadata.create_all) 247 | ``` 248 | 249 | ## CRUD операции (async) 250 | 251 | ### Create 252 | 253 | ```python 254 | async with AsyncSessionLocal() as session: 255 | user = User(username="alice", email="alice@example.com") 256 | session.add(user) 257 | await session.commit() 258 | ``` 259 | 260 | ### Read 261 | 262 | ```python 263 | async with AsyncSessionLocal() as session: 264 | # По ID 265 | user = await session.get(User, 1) 266 | 267 | # С фильтром 268 | stmt = select(User).where(User.username == "alice") 269 | result = await session.execute(stmt) 270 | user = result.scalar_one() 271 | 272 | # Все записи 273 | stmt = select(User) 274 | result = await session.execute(stmt) 275 | users = result.scalars().all() 276 | ``` 277 | 278 | ### Update 279 | 280 | ```python 281 | async with AsyncSessionLocal() as session: 282 | user = await session.get(User, 1) 283 | user.email = "new@example.com" 284 | await session.commit() 285 | ``` 286 | 287 | ### Delete 288 | 289 | ```python 290 | async with AsyncSessionLocal() as session: 291 | user = await session.get(User, 1) 292 | await session.delete(user) 293 | await session.commit() 294 | ``` 295 | 296 | ## Connection Pooling 297 | 298 | ```python 299 | async_engine = create_async_engine( 300 | DATABASE_URL, 301 | 302 | # Размер пула 303 | pool_size=5, # Постоянные соединения 304 | max_overflow=10, # Дополнительные при нужде 305 | 306 | # Таймауты 307 | pool_timeout=30, # Ожидание свободного соединения 308 | pool_recycle=3600, # Переподключение через час 309 | 310 | # Проверка соединения 311 | pool_pre_ping=True, # Проверять перед использованием 312 | 313 | # Логирование 314 | echo=True, # Показывать SQL 315 | ) 316 | ``` 317 | 318 | ## Производительность 319 | 320 | ### Batch операции 321 | 322 | ```python 323 | # Создание множества записей 324 | async with AsyncSessionLocal() as session: 325 | users = [User(username=f"user{i}") for i in range(1000)] 326 | session.add_all(users) 327 | await session.commit() 328 | 329 | # Массовое обновление 330 | async with AsyncSessionLocal() as session: 331 | stmt = update(User).where(User.age < 18).values(status="minor") 332 | await session.execute(stmt) 333 | await session.commit() 334 | ``` 335 | 336 | ### Оптимизация N+1 337 | 338 | ```python 339 | # ❌ Плохо: N+1 запросов 340 | async with AsyncSessionLocal() as session: 341 | users = (await session.execute(select(User))).scalars().all() 342 | for user in users: 343 | # Каждая итерация = новый запрос! 344 | posts = await user.awaitable_attrs.posts 345 | 346 | # ✅ Хорошо: 1 запрос 347 | async with AsyncSessionLocal() as session: 348 | stmt = select(User).options(selectinload(User.posts)) 349 | users = (await session.execute(stmt)).scalars().all() 350 | for user in users: 351 | # Посты уже загружены 352 | for post in user.posts: 353 | print(post.title) 354 | ``` 355 | 356 | ## Запуск примеров 357 | 358 | ### SQLite (aiosqlite) 359 | 360 | ```bash 361 | # Установка зависимостей 362 | pip install sqlalchemy[asyncio] aiosqlite 363 | 364 | # Запуск 365 | python async_sqlite.py 366 | ``` 367 | 368 | ### PostgreSQL (asyncpg) 369 | 370 | ```bash 371 | # Установка зависимостей 372 | pip install sqlalchemy[asyncio] asyncpg 373 | 374 | # Убедитесь что PostgreSQL запущен 375 | # Обновите DATABASE_URL в async_postgres.py 376 | 377 | # Запуск 378 | python async_postgres.py 379 | ``` 380 | 381 | ## Интеграция с FastAPI 382 | 383 | ```python 384 | from fastapi import FastAPI, Depends 385 | from sqlalchemy.ext.asyncio import AsyncSession 386 | 387 | app = FastAPI() 388 | 389 | async def get_session() -> AsyncSession: 390 | async with AsyncSessionLocal() as session: 391 | yield session 392 | 393 | @app.get("/users/{user_id}") 394 | async def get_user(user_id: int, session: AsyncSession = Depends(get_session)): 395 | user = await session.get(User, user_id) 396 | return {"username": user.username, "email": user.email} 397 | 398 | @app.get("/users") 399 | async def list_users(session: AsyncSession = Depends(get_session)): 400 | result = await session.execute(select(User)) 401 | users = result.scalars().all() 402 | return [{"id": u.id, "username": u.username} for u in users] 403 | ``` 404 | 405 | ## Лучшие практики 406 | 407 | 1. ✅ **Используйте selectinload** - предотвращает N+1 проблему 408 | 2. ✅ **Закрывайте сессии** - используйте async context managers 409 | 3. ✅ **Connection pooling** - настраивайте для вашей нагрузки 410 | 4. ✅ **Обрабатывайте ошибки** - используйте try/except и rollback 411 | 5. ✅ **Тестируйте производительность** - измеряйте реальную нагрузку 412 | 6. ❌ **Не смешивайте sync/async** - используйте только async код 413 | 7. ❌ **Не забывайте await** - все операции с БД асинхронные 414 | 415 | ## Сравнение производительности 416 | 417 | ### Синхронный код 418 | ```python 419 | # 10 запросов последовательно 420 | for i in range(10): 421 | user = session.get(User, i) 422 | # Время: ~1 секунда 423 | ``` 424 | 425 | ### Асинхронный код 426 | ```python 427 | # 10 запросов параллельно 428 | tasks = [session.get(User, i) for i in range(10)] 429 | users = await asyncio.gather(*tasks) 430 | # Время: ~0.1 секунды (10x быстрее!) 431 | ``` 432 | 433 | ## Debugging 434 | 435 | ### Включить логирование SQL 436 | 437 | ```python 438 | async_engine = create_async_engine( 439 | DATABASE_URL, 440 | echo=True, # Показывать все SQL запросы 441 | ) 442 | ``` 443 | 444 | ### Отслеживание соединений 445 | 446 | ```python 447 | # Проверить статус пула 448 | print(f"Pool size: {async_engine.pool.size()}") 449 | print(f"Checked out: {async_engine.pool.checkedout()}") 450 | ``` 451 | 452 | ## Ресурсы 453 | 454 | - [SQLAlchemy Async Documentation](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html) 455 | - [asyncpg Documentation](https://magicstack.github.io/asyncpg/) 456 | - [aiosqlite Documentation](https://aiosqlite.omnilib.dev/) 457 | - [Python asyncio Documentation](https://docs.python.org/3/library/asyncio.html) 458 | 459 | ## Заключение 460 | 461 | Асинхронный SQLAlchemy - мощный инструмент для высоконагруженных приложений. Основные концепции: 462 | 463 | - **async/await** - для всех операций с БД 464 | - **AsyncSession** - асинхронная сессия 465 | - **selectinload** - eager loading для relationships 466 | - **Connection pooling** - эффективное управление соединениями 467 | - **Transaction management** - безопасная работа с транзакциями 468 | -------------------------------------------------------------------------------- /example_08/SYNC_VS_ASYNC.md: -------------------------------------------------------------------------------- 1 | # SQLAlchemy: Синхронный vs Асинхронный код 2 | 3 | Полное сравнение синхронного и асинхронного подходов в SQLAlchemy 2.0. 4 | 5 | ## 1. Импорты 6 | 7 | ### Синхронный 8 | ```python 9 | from sqlalchemy import create_engine, String, select 10 | from sqlalchemy.orm import Session, DeclarativeBase, Mapped, mapped_column 11 | ``` 12 | 13 | ### Асинхронный 14 | ```python 15 | from sqlalchemy import String, select 16 | from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker 17 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column 18 | ``` 19 | 20 | ## 2. Engine 21 | 22 | ### Синхронный 23 | ```python 24 | engine = create_engine('sqlite:///database.db', echo=True) 25 | ``` 26 | 27 | ### Асинхронный 28 | ```python 29 | async_engine = create_async_engine('sqlite+aiosqlite:///database.db', echo=True) 30 | ``` 31 | 32 | **Драйверы:** 33 | | БД | Синхронный | Асинхронный | 34 | |---|---|---| 35 | | SQLite | `sqlite://` | `sqlite+aiosqlite://` | 36 | | PostgreSQL | `postgresql://` | `postgresql+asyncpg://` | 37 | | MySQL | `mysql://` | `mysql+aiomysql://` | 38 | 39 | ## 3. Создание таблиц 40 | 41 | ### Синхронный 42 | ```python 43 | Base.metadata.create_all(engine) 44 | ``` 45 | 46 | ### Асинхронный 47 | ```python 48 | async with async_engine.begin() as conn: 49 | await conn.run_sync(Base.metadata.create_all) 50 | ``` 51 | 52 | ## 4. Session 53 | 54 | ### Синхронный 55 | ```python 56 | # Создание session 57 | with Session(engine) as session: 58 | # работа с БД 59 | session.commit() 60 | ``` 61 | 62 | ### Асинхронный 63 | ```python 64 | # Создание async session maker 65 | AsyncSessionLocal = async_sessionmaker(async_engine, expire_on_commit=False) 66 | 67 | # Использование 68 | async with AsyncSessionLocal() as session: 69 | # работа с БД 70 | await session.commit() 71 | ``` 72 | 73 | ## 5. CREATE (Создание) 74 | 75 | ### Синхронный 76 | ```python 77 | with Session(engine) as session: 78 | user = User(username="alice", email="alice@example.com") 79 | session.add(user) 80 | session.commit() 81 | print(f"Created user ID: {user.id}") 82 | ``` 83 | 84 | ### Асинхронный 85 | ```python 86 | async with AsyncSessionLocal() as session: 87 | user = User(username="alice", email="alice@example.com") 88 | session.add(user) 89 | await session.commit() 90 | print(f"Created user ID: {user.id}") 91 | ``` 92 | 93 | **Разница:** Добавили `await` перед `commit()` 94 | 95 | ## 6. READ (Чтение) 96 | 97 | ### Синхронный 98 | ```python 99 | with Session(engine) as session: 100 | # По ID 101 | user = session.get(User, 1) 102 | 103 | # С фильтром 104 | stmt = select(User).where(User.username == "alice") 105 | user = session.execute(stmt).scalar_one() 106 | 107 | # Все записи 108 | stmt = select(User) 109 | users = session.execute(stmt).scalars().all() 110 | ``` 111 | 112 | ### Асинхронный 113 | ```python 114 | async with AsyncSessionLocal() as session: 115 | # По ID 116 | user = await session.get(User, 1) 117 | 118 | # С фильтром 119 | stmt = select(User).where(User.username == "alice") 120 | result = await session.execute(stmt) 121 | user = result.scalar_one() 122 | 123 | # Все записи 124 | stmt = select(User) 125 | result = await session.execute(stmt) 126 | users = result.scalars().all() 127 | ``` 128 | 129 | **Разница:** Добавили `await` перед `get()` и `execute()` 130 | 131 | ## 7. UPDATE (Обновление) 132 | 133 | ### Синхронный 134 | ```python 135 | with Session(engine) as session: 136 | user = session.get(User, 1) 137 | user.email = "new@example.com" 138 | session.commit() 139 | ``` 140 | 141 | ### Асинхронный 142 | ```python 143 | async with AsyncSessionLocal() as session: 144 | user = await session.get(User, 1) 145 | user.email = "new@example.com" 146 | await session.commit() 147 | ``` 148 | 149 | **Разница:** `await` перед `get()` и `commit()` 150 | 151 | ## 8. DELETE (Удаление) 152 | 153 | ### Синхронный 154 | ```python 155 | with Session(engine) as session: 156 | user = session.get(User, 1) 157 | session.delete(user) 158 | session.commit() 159 | ``` 160 | 161 | ### Асинхронный 162 | ```python 163 | async with AsyncSessionLocal() as session: 164 | user = await session.get(User, 1) 165 | await session.delete(user) 166 | await session.commit() 167 | ``` 168 | 169 | **Разница:** `await` перед `get()`, `delete()` и `commit()` 170 | 171 | ## 9. Relationships (Связи) 172 | 173 | ### Синхронный 174 | ```python 175 | # Определение 176 | class User(Base): 177 | __tablename__ = 'users' 178 | id: Mapped[int] = mapped_column(primary_key=True) 179 | posts: Mapped[List["Post"]] = relationship(back_populates="author") 180 | 181 | # Использование - ленивая загрузка работает автоматически 182 | with Session(engine) as session: 183 | user = session.get(User, 1) 184 | for post in user.posts: # Автоматически загружаются 185 | print(post.title) 186 | ``` 187 | 188 | ### Асинхронный 189 | ```python 190 | # Определение - то же самое 191 | class User(Base): 192 | __tablename__ = 'users' 193 | id: Mapped[int] = mapped_column(primary_key=True) 194 | posts: Mapped[List["Post"]] = relationship(back_populates="author") 195 | 196 | # Использование - НУЖЕН selectinload! 197 | from sqlalchemy.orm import selectinload 198 | 199 | async with AsyncSessionLocal() as session: 200 | stmt = select(User).options(selectinload(User.posts)).where(User.id == 1) 201 | result = await session.execute(stmt) 202 | user = result.scalar_one() 203 | 204 | for post in user.posts: # Уже загружены 205 | print(post.title) 206 | ``` 207 | 208 | **Важно:** В async НЕЛЬЗЯ использовать ленивую загрузку! Всегда используйте `selectinload()`. 209 | 210 | ### Альтернатива: AsyncAttrs 211 | 212 | ```python 213 | from sqlalchemy.ext.asyncio import AsyncAttrs 214 | 215 | class Base(AsyncAttrs, DeclarativeBase): 216 | pass 217 | 218 | # Тогда можно: 219 | async with AsyncSessionLocal() as session: 220 | user = await session.get(User, 1) 221 | posts = await user.awaitable_attrs.posts # Async lazy loading 222 | ``` 223 | 224 | ## 10. Вложенные связи 225 | 226 | ### Синхронный 227 | ```python 228 | with Session(engine) as session: 229 | users = session.execute( 230 | select(User) 231 | .options(joinedload(User.posts).joinedload(Post.comments)) 232 | ).unique().scalars().all() 233 | 234 | for user in users: 235 | for post in user.posts: 236 | for comment in post.comments: 237 | print(comment.content) 238 | ``` 239 | 240 | ### Асинхронный 241 | ```python 242 | async with AsyncSessionLocal() as session: 243 | stmt = select(User).options( 244 | selectinload(User.posts).selectinload(Post.comments) 245 | ) 246 | result = await session.execute(stmt) 247 | users = result.scalars().all() 248 | 249 | for user in users: 250 | for post in user.posts: 251 | for comment in post.comments: 252 | print(comment.content) 253 | ``` 254 | 255 | **Рекомендация:** В async используйте `selectinload` вместо `joinedload` 256 | 257 | ## 11. Транзакции 258 | 259 | ### Синхронный 260 | ```python 261 | with Session(engine) as session: 262 | with session.begin(): 263 | user = User(username="alice") 264 | session.add(user) 265 | # Автоматический commit или rollback 266 | ``` 267 | 268 | ### Асинхронный 269 | ```python 270 | async with AsyncSessionLocal() as session: 271 | async with session.begin(): 272 | user = User(username="alice") 273 | session.add(user) 274 | # Автоматический commit или rollback 275 | ``` 276 | 277 | **Разница:** `async with` вместо `with` 278 | 279 | ## 12. Агрегация 280 | 281 | ### Синхронный 282 | ```python 283 | from sqlalchemy import func 284 | 285 | with Session(engine) as session: 286 | count = session.execute( 287 | select(func.count()).select_from(User) 288 | ).scalar() 289 | 290 | avg_age = session.execute( 291 | select(func.avg(User.age)) 292 | ).scalar() 293 | ``` 294 | 295 | ### Асинхронный 296 | ```python 297 | from sqlalchemy import func 298 | 299 | async with AsyncSessionLocal() as session: 300 | result = await session.execute( 301 | select(func.count()).select_from(User) 302 | ) 303 | count = result.scalar() 304 | 305 | result = await session.execute( 306 | select(func.avg(User.age)) 307 | ) 308 | avg_age = result.scalar() 309 | ``` 310 | 311 | **Разница:** `await` перед `execute()` 312 | 313 | ## 13. Batch операции 314 | 315 | ### Синхронный 316 | ```python 317 | with Session(engine) as session: 318 | users = [User(username=f"user{i}") for i in range(100)] 319 | session.add_all(users) 320 | session.commit() 321 | ``` 322 | 323 | ### Асинхронный 324 | ```python 325 | async with AsyncSessionLocal() as session: 326 | users = [User(username=f"user{i}") for i in range(100)] 327 | session.add_all(users) 328 | await session.commit() 329 | ``` 330 | 331 | **Разница:** `await` перед `commit()` 332 | 333 | ## 14. Streaming (для больших результатов) 334 | 335 | ### Синхронный 336 | ```python 337 | with Session(engine) as session: 338 | for user in session.execute(select(User)).scalars(): 339 | process(user) 340 | ``` 341 | 342 | ### Асинхронный 343 | ```python 344 | async with AsyncSessionLocal() as session: 345 | result = await session.stream(select(User)) 346 | 347 | async for (user,) in result: 348 | process(user) 349 | ``` 350 | 351 | **Разница:** `await session.stream()` и `async for` 352 | 353 | ## 15. Конкурентные операции 354 | 355 | ### Синхронный 356 | ```python 357 | # Выполняется последовательно 358 | with Session(engine) as session: 359 | user1 = session.get(User, 1) 360 | user2 = session.get(User, 2) 361 | user3 = session.get(User, 3) 362 | # Время: 3 × T 363 | ``` 364 | 365 | ### Асинхронный 366 | ```python 367 | import asyncio 368 | 369 | # Выполняется параллельно! 370 | async def get_user(session, user_id): 371 | return await session.get(User, user_id) 372 | 373 | async with AsyncSessionLocal() as session: 374 | results = await asyncio.gather( 375 | get_user(session, 1), 376 | get_user(session, 2), 377 | get_user(session, 3), 378 | ) 379 | # Время: ≈ T 380 | ``` 381 | 382 | **Преимущество:** Async может выполнять операции параллельно! 383 | 384 | ## 16. Закрытие соединений 385 | 386 | ### Синхронный 387 | ```python 388 | engine.dispose() 389 | ``` 390 | 391 | ### Асинхронный 392 | ```python 393 | await async_engine.dispose() 394 | ``` 395 | 396 | **Разница:** `await` перед `dispose()` 397 | 398 | ## 17. Тестирование 399 | 400 | ### Синхронный 401 | ```python 402 | def test_create_user(): 403 | with Session(engine) as session: 404 | user = User(username="test") 405 | session.add(user) 406 | session.commit() 407 | assert user.id is not None 408 | ``` 409 | 410 | ### Асинхронный 411 | ```python 412 | import pytest 413 | 414 | @pytest.mark.asyncio 415 | async def test_create_user(): 416 | async with AsyncSessionLocal() as session: 417 | user = User(username="test") 418 | session.add(user) 419 | await session.commit() 420 | assert user.id is not None 421 | ``` 422 | 423 | **Требует:** `pip install pytest-asyncio` 424 | 425 | ## Сводная таблица 426 | 427 | | Операция | Синхронный | Асинхронный | 428 | |---|---|---| 429 | | Engine | `create_engine()` | `create_async_engine()` | 430 | | Session | `Session()` | `AsyncSession()` | 431 | | Context manager | `with` | `async with` | 432 | | Запросы | `execute()` | `await execute()` | 433 | | Commit | `commit()` | `await commit()` | 434 | | Get by ID | `get()` | `await get()` | 435 | | Delete | `delete()` | `await delete()` | 436 | | Relationships | Auto lazy load | `selectinload()` обязателен | 437 | | Итерация | `for item in items` | `async for item in items` | 438 | | Параллелизм | ❌ Последовательно | ✅ `asyncio.gather()` | 439 | 440 | ## Когда использовать что? 441 | 442 | ### Используйте Синхронный если: 443 | - 🔧 Простые скрипты и утилиты 444 | - 📊 Data science / аналитика 445 | - 🎓 Обучение / прототипирование 446 | - 👥 Команда не знакома с async 447 | - 📉 Низкая нагрузка 448 | 449 | ### Используйте Асинхронный если: 450 | - 🚀 Веб-приложения (FastAPI, aiohttp) 451 | - 📈 Высокая нагрузка 452 | - ⚡ Нужна параллельность 453 | - 🔄 Микросервисы 454 | - 💬 Real-time приложения 455 | 456 | ## Производительность 457 | 458 | ```python 459 | # Синхронный: 10 запросов последовательно 460 | for i in range(10): 461 | user = session.get(User, i) # ~100ms каждый 462 | # Общее время: ~1000ms 463 | 464 | # Асинхронный: 10 запросов параллельно 465 | tasks = [session.get(User, i) for i in range(10)] 466 | users = await asyncio.gather(*tasks) 467 | # Общее время: ~100ms (10x быстрее!) 468 | ``` 469 | 470 | ## Миграция с Sync на Async 471 | 472 | 1. ✅ Замените `create_engine` на `create_async_engine` 473 | 2. ✅ Добавьте `async`/`await` ко всем операциям с БД 474 | 3. ✅ Замените `with` на `async with` 475 | 4. ✅ Добавьте `selectinload()` для relationships 476 | 5. ✅ Обновите URL драйвера (`sqlite+aiosqlite://`) 477 | 6. ✅ Установите async драйверы (`aiosqlite`, `asyncpg`) 478 | 479 | ## Заключение 480 | 481 | **Синхронный код проще**, но **асинхронный производительнее** при высокой нагрузке. 482 | 483 | Выбирайте подход в зависимости от требований проекта! 484 | -------------------------------------------------------------------------------- /example_03/one_to_one.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 03: Связь One-to-One (1-1) 3 | Демонстрирует реализацию связи один-к-одному между таблицами 4 | """ 5 | 6 | from sqlalchemy import create_engine, Column, Integer, String, ForeignKey, Text, Date 7 | from sqlalchemy.orm import relationship, declarative_base, sessionmaker 8 | from datetime import date 9 | 10 | 11 | # ============================================ 12 | # Настройка базы данных 13 | # ============================================ 14 | 15 | Base = declarative_base() 16 | engine = create_engine('sqlite:///one_to_one.db', echo=False) 17 | Session = sessionmaker(bind=engine) 18 | 19 | 20 | # ============================================ 21 | # Пример 1: Пользователь и Профиль 22 | # ============================================ 23 | 24 | class User(Base): 25 | """Пользователь""" 26 | __tablename__ = 'users' 27 | 28 | id = Column(Integer, primary_key=True) 29 | username = Column(String(50), unique=True, nullable=False) 30 | email = Column(String(100), unique=True, nullable=False) 31 | 32 | # One-to-One связь с профилем 33 | # uselist=False делает связь 1-1 (вместо 1-Many) 34 | profile = relationship( 35 | "UserProfile", 36 | back_populates="user", 37 | uselist=False, 38 | cascade="all, delete-orphan" # При удалении User удаляется и Profile 39 | ) 40 | 41 | def __repr__(self): 42 | return f"" 43 | 44 | 45 | class UserProfile(Base): 46 | """Профиль пользователя""" 47 | __tablename__ = 'user_profiles' 48 | 49 | id = Column(Integer, primary_key=True) 50 | 51 | # ForeignKey с UNIQUE constraint для обеспечения связи 1-1 52 | user_id = Column(Integer, ForeignKey('users.id'), unique=True, nullable=False) 53 | 54 | first_name = Column(String(50)) 55 | last_name = Column(String(50)) 56 | bio = Column(Text) 57 | avatar_url = Column(String(200)) 58 | birth_date = Column(Date) 59 | 60 | # Обратная связь 61 | user = relationship("User", back_populates="profile") 62 | 63 | def __repr__(self): 64 | return f"" 65 | 66 | 67 | # ============================================ 68 | # Пример 2: Сотрудник и Рабочее место 69 | # ============================================ 70 | 71 | class Employee(Base): 72 | """Сотрудник""" 73 | __tablename__ = 'employees' 74 | 75 | id = Column(Integer, primary_key=True) 76 | name = Column(String(100), nullable=False) 77 | position = Column(String(100)) 78 | 79 | # One-to-One связь с рабочим местом 80 | workstation = relationship( 81 | "Workstation", 82 | back_populates="employee", 83 | uselist=False, 84 | cascade="all, delete-orphan" 85 | ) 86 | 87 | def __repr__(self): 88 | return f"" 89 | 90 | 91 | class Workstation(Base): 92 | """Рабочее место""" 93 | __tablename__ = 'workstations' 94 | 95 | id = Column(Integer, primary_key=True) 96 | employee_id = Column(Integer, ForeignKey('employees.id'), unique=True) 97 | 98 | room_number = Column(String(10)) 99 | desk_number = Column(Integer) 100 | computer_id = Column(String(50)) 101 | 102 | employee = relationship("Employee", back_populates="workstation") 103 | 104 | def __repr__(self): 105 | return f"" 106 | 107 | 108 | # ============================================ 109 | # Пример 3: Паспорт и Владелец 110 | # ============================================ 111 | 112 | class Person(Base): 113 | """Человек""" 114 | __tablename__ = 'persons' 115 | 116 | id = Column(Integer, primary_key=True) 117 | name = Column(String(100), nullable=False) 118 | 119 | passport = relationship( 120 | "Passport", 121 | back_populates="owner", 122 | uselist=False, 123 | cascade="all, delete-orphan" 124 | ) 125 | 126 | def __repr__(self): 127 | return f"" 128 | 129 | 130 | class Passport(Base): 131 | """Паспорт""" 132 | __tablename__ = 'passports' 133 | 134 | id = Column(Integer, primary_key=True) 135 | person_id = Column(Integer, ForeignKey('persons.id'), unique=True, nullable=False) 136 | 137 | number = Column(String(20), unique=True, nullable=False) 138 | issued_date = Column(Date) 139 | expiry_date = Column(Date) 140 | country = Column(String(50)) 141 | 142 | owner = relationship("Person", back_populates="passport") 143 | 144 | def __repr__(self): 145 | return f"" 146 | 147 | 148 | # ============================================ 149 | # Демонстрация использования 150 | # ============================================ 151 | 152 | def demo_user_profile(): 153 | """Демонстрация связи User-Profile""" 154 | print("\n" + "=" * 50) 155 | print("Demo 1: User and Profile (1-1)") 156 | print("=" * 50) 157 | 158 | session = Session() 159 | 160 | try: 161 | # 1. Создание пользователя с профилем 162 | print("\n1. Creating user with profile...") 163 | user = User( 164 | username="john_doe", 165 | email="john@example.com" 166 | ) 167 | 168 | profile = UserProfile( 169 | first_name="John", 170 | last_name="Doe", 171 | bio="Software Engineer passionate about Python", 172 | avatar_url="/avatars/john.jpg", 173 | birth_date=date(1990, 5, 15) 174 | ) 175 | 176 | # Связывание 177 | user.profile = profile 178 | 179 | session.add(user) 180 | session.commit() 181 | print(f"✓ Created: {user}") 182 | print(f"✓ Profile: {user.profile}") 183 | 184 | # 2. Доступ через обратную связь 185 | print("\n2. Accessing user from profile...") 186 | profile_from_db = session.query(UserProfile).first() 187 | print(f"Profile belongs to: {profile_from_db.user}") 188 | 189 | # 3. Создание без явного связывания 190 | print("\n3. Creating user with profile (alternative way)...") 191 | user2 = User( 192 | username="alice_smith", 193 | email="alice@example.com", 194 | profile=UserProfile( 195 | first_name="Alice", 196 | last_name="Smith", 197 | bio="Data Scientist", 198 | birth_date=date(1992, 8, 20) 199 | ) 200 | ) 201 | session.add(user2) 202 | session.commit() 203 | print(f"✓ Created: {user2}") 204 | 205 | # 4. Проверка связи 206 | print("\n4. Checking relationships...") 207 | users = session.query(User).all() 208 | for u in users: 209 | print(f"User: {u.username}") 210 | if u.profile: 211 | print(f" Profile: {u.profile.first_name} {u.profile.last_name}") 212 | print(f" Bio: {u.profile.bio}") 213 | else: 214 | print(" No profile") 215 | 216 | finally: 217 | session.close() 218 | 219 | 220 | def demo_employee_workstation(): 221 | """Демонстрация связи Employee-Workstation""" 222 | print("\n" + "=" * 50) 223 | print("Demo 2: Employee and Workstation (1-1)") 224 | print("=" * 50) 225 | 226 | session = Session() 227 | 228 | try: 229 | # Создание сотрудников с рабочими местами 230 | print("\n1. Creating employees with workstations...") 231 | 232 | emp1 = Employee( 233 | name="Bob Johnson", 234 | position="Software Developer", 235 | workstation=Workstation( 236 | room_number="101", 237 | desk_number=5, 238 | computer_id="PC-1001" 239 | ) 240 | ) 241 | 242 | emp2 = Employee( 243 | name="Carol White", 244 | position="Project Manager", 245 | workstation=Workstation( 246 | room_number="102", 247 | desk_number=1, 248 | computer_id="PC-1002" 249 | ) 250 | ) 251 | 252 | session.add_all([emp1, emp2]) 253 | session.commit() 254 | 255 | # Вывод информации 256 | print("\n2. Employees and their workstations:") 257 | employees = session.query(Employee).all() 258 | for emp in employees: 259 | print(f"\n{emp.name} ({emp.position})") 260 | if emp.workstation: 261 | ws = emp.workstation 262 | print(f" Workstation: Room {ws.room_number}, Desk #{ws.desk_number}") 263 | print(f" Computer: {ws.computer_id}") 264 | 265 | finally: 266 | session.close() 267 | 268 | 269 | def demo_person_passport(): 270 | """Демонстрация связи Person-Passport""" 271 | print("\n" + "=" * 50) 272 | print("Demo 3: Person and Passport (1-1)") 273 | print("=" * 50) 274 | 275 | session = Session() 276 | 277 | try: 278 | # Создание людей с паспортами 279 | print("\n1. Creating persons with passports...") 280 | 281 | person1 = Person( 282 | name="Elena Rodriguez", 283 | passport=Passport( 284 | number="AB123456", 285 | issued_date=date(2020, 1, 15), 286 | expiry_date=date(2030, 1, 15), 287 | country="Spain" 288 | ) 289 | ) 290 | 291 | person2 = Person( 292 | name="David Chen", 293 | passport=Passport( 294 | number="CD789012", 295 | issued_date=date(2019, 6, 20), 296 | expiry_date=date(2029, 6, 20), 297 | country="China" 298 | ) 299 | ) 300 | 301 | session.add_all([person1, person2]) 302 | session.commit() 303 | 304 | # Вывод информации 305 | print("\n2. Persons and their passports:") 306 | persons = session.query(Person).all() 307 | for person in persons: 308 | print(f"\n{person.name}") 309 | if person.passport: 310 | p = person.passport 311 | print(f" Passport: {p.number} ({p.country})") 312 | print(f" Valid: {p.issued_date} to {p.expiry_date}") 313 | 314 | finally: 315 | session.close() 316 | 317 | 318 | def demo_cascade_delete(): 319 | """Демонстрация каскадного удаления""" 320 | print("\n" + "=" * 50) 321 | print("Demo 4: Cascade Delete") 322 | print("=" * 50) 323 | 324 | session = Session() 325 | 326 | try: 327 | # Создание пользователя 328 | user = User( 329 | username="temp_user", 330 | email="temp@example.com", 331 | profile=UserProfile( 332 | first_name="Temp", 333 | last_name="User", 334 | bio="Temporary account" 335 | ) 336 | ) 337 | session.add(user) 338 | session.commit() 339 | 340 | user_id = user.id 341 | profile_id = user.profile.id 342 | 343 | print(f"\n1. Created user (id={user_id}) with profile (id={profile_id})") 344 | 345 | # Проверка наличия 346 | profile_count = session.query(UserProfile).filter_by(id=profile_id).count() 347 | print(f"2. Profile exists: {profile_count > 0}") 348 | 349 | # Удаление пользователя (профиль удалится автоматически благодаря cascade) 350 | session.delete(user) 351 | session.commit() 352 | 353 | print(f"3. Deleted user (id={user_id})") 354 | 355 | # Проверка, что профиль тоже удален 356 | profile_count = session.query(UserProfile).filter_by(id=profile_id).count() 357 | print(f"4. Profile exists after user deletion: {profile_count > 0}") 358 | print("✓ Cascade delete worked!") 359 | 360 | finally: 361 | session.close() 362 | 363 | 364 | def demo_querying(): 365 | """Демонстрация различных запросов""" 366 | print("\n" + "=" * 50) 367 | print("Demo 5: Querying Relationships") 368 | print("=" * 50) 369 | 370 | session = Session() 371 | 372 | try: 373 | # 1. Найти пользователей с профилем 374 | print("\n1. Users with profiles:") 375 | users_with_profile = session.query(User).filter(User.profile != None).all() 376 | print(f"Found {len(users_with_profile)} users with profiles") 377 | 378 | # 2. Найти пользователей без профиля 379 | print("\n2. Users without profiles:") 380 | users_without_profile = session.query(User).filter(User.profile == None).all() 381 | print(f"Found {len(users_without_profile)} users without profiles") 382 | 383 | # 3. JOIN запрос 384 | print("\n3. JOIN query (users with their profiles):") 385 | results = session.query(User, UserProfile).join(UserProfile).all() 386 | for user, profile in results: 387 | print(f" {user.username}: {profile.first_name} {profile.last_name}") 388 | 389 | # 4. Фильтрация по связанному объекту 390 | print("\n4. Filter by related object:") 391 | users = session.query(User).join(UserProfile).filter( 392 | UserProfile.last_name == "Doe" 393 | ).all() 394 | for user in users: 395 | print(f" {user.username}") 396 | 397 | finally: 398 | session.close() 399 | 400 | 401 | # ============================================ 402 | # Main 403 | # ============================================ 404 | 405 | if __name__ == "__main__": 406 | print("=" * 50) 407 | print("SQLAlchemy One-to-One Relationship Example") 408 | print("=" * 50) 409 | 410 | # Создание таблиц 411 | Base.metadata.drop_all(engine) 412 | Base.metadata.create_all(engine) 413 | 414 | # Запуск демонстраций 415 | demo_user_profile() 416 | demo_employee_workstation() 417 | demo_person_passport() 418 | demo_cascade_delete() 419 | demo_querying() 420 | 421 | print("\n" + "=" * 50) 422 | print("All demos completed!") 423 | print("Database: one_to_one.db") 424 | print("=" * 50) 425 | -------------------------------------------------------------------------------- /example_08/async_postgres.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 08: Асинхронные запросы с SQLAlchemy 2.0 + asyncpg (PostgreSQL) 3 | Демонстрирует использование async/await с PostgreSQL через asyncpg 4 | """ 5 | 6 | import asyncio 7 | from typing import Optional, List 8 | from datetime import datetime, timezone 9 | from decimal import Decimal 10 | 11 | from sqlalchemy import String, Numeric, select, update, delete, func, Index 12 | from sqlalchemy.ext.asyncio import ( 13 | create_async_engine, 14 | async_sessionmaker, 15 | AsyncSession, 16 | AsyncAttrs, 17 | ) 18 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, selectinload 19 | 20 | 21 | # ============================================ 22 | # SQLAlchemy 2.0 Async Base with AsyncAttrs 23 | # ============================================ 24 | 25 | class Base(AsyncAttrs, DeclarativeBase): 26 | """ 27 | AsyncAttrs добавляет поддержку асинхронной загрузки атрибутов 28 | Позволяет использовать: await user.awaitable_attrs.posts 29 | """ 30 | pass 31 | 32 | 33 | # PostgreSQL async connection 34 | # Формат: postgresql+asyncpg://user:password@host:port/database 35 | DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/async_db" 36 | 37 | async_engine = create_async_engine( 38 | DATABASE_URL, 39 | echo=True, 40 | pool_size=5, 41 | max_overflow=10, 42 | pool_pre_ping=True, # Проверка соединения перед использованием 43 | ) 44 | 45 | AsyncSessionLocal = async_sessionmaker( 46 | async_engine, 47 | class_=AsyncSession, 48 | expire_on_commit=False 49 | ) 50 | 51 | 52 | # ============================================ 53 | # Модели 54 | # ============================================ 55 | 56 | class User(Base): 57 | """Модель пользователя""" 58 | __tablename__ = 'users' 59 | 60 | id: Mapped[int] = mapped_column(primary_key=True) 61 | username: Mapped[str] = mapped_column(String(50), unique=True, index=True) 62 | email: Mapped[str] = mapped_column(String(100), unique=True) 63 | age: Mapped[Optional[int]] 64 | is_active: Mapped[bool] = mapped_column(default=True) 65 | balance: Mapped[Decimal] = mapped_column(Numeric(10, 2), default=Decimal('0.00')) 66 | created_at: Mapped[datetime] = mapped_column( 67 | default=lambda: datetime.now(timezone.utc), 68 | index=True 69 | ) 70 | updated_at: Mapped[Optional[datetime]] = mapped_column( 71 | onupdate=lambda: datetime.now(timezone.utc) 72 | ) 73 | 74 | # Relationships 75 | posts: Mapped[List["Post"]] = relationship( 76 | back_populates="author", 77 | cascade="all, delete-orphan", 78 | lazy="selectin" # По умолчанию используем selectin 79 | ) 80 | comments: Mapped[List["Comment"]] = relationship( 81 | back_populates="author", 82 | cascade="all, delete-orphan" 83 | ) 84 | 85 | def __repr__(self) -> str: 86 | return f"" 87 | 88 | 89 | class Post(Base): 90 | """Модель поста""" 91 | __tablename__ = 'posts' 92 | 93 | id: Mapped[int] = mapped_column(primary_key=True) 94 | title: Mapped[str] = mapped_column(String(200), index=True) 95 | content: Mapped[str] 96 | author_id: Mapped[int] = mapped_column(index=True) 97 | views: Mapped[int] = mapped_column(default=0) 98 | published: Mapped[bool] = mapped_column(default=False) 99 | created_at: Mapped[datetime] = mapped_column( 100 | default=lambda: datetime.now(timezone.utc) 101 | ) 102 | 103 | # Relationships 104 | author: Mapped["User"] = relationship(back_populates="posts") 105 | comments: Mapped[List["Comment"]] = relationship( 106 | back_populates="post", 107 | cascade="all, delete-orphan" 108 | ) 109 | 110 | def __repr__(self) -> str: 111 | return f"" 112 | 113 | 114 | class Comment(Base): 115 | """Модель комментария""" 116 | __tablename__ = 'comments' 117 | 118 | id: Mapped[int] = mapped_column(primary_key=True) 119 | content: Mapped[str] 120 | post_id: Mapped[int] = mapped_column(index=True) 121 | author_id: Mapped[int] = mapped_column(index=True) 122 | created_at: Mapped[datetime] = mapped_column( 123 | default=lambda: datetime.now(timezone.utc) 124 | ) 125 | 126 | # Relationships 127 | post: Mapped["Post"] = relationship(back_populates="comments") 128 | author: Mapped["User"] = relationship(back_populates="comments") 129 | 130 | def __repr__(self) -> str: 131 | return f"" 132 | 133 | 134 | # Составные индексы 135 | Index('idx_post_author_published', Post.author_id, Post.published) 136 | 137 | 138 | # ============================================ 139 | # CREATE операции 140 | # ============================================ 141 | 142 | async def create_sample_data(): 143 | """Создание тестовых данных""" 144 | print("\n" + "=" * 50) 145 | print("CREATE Sample Data (Async PostgreSQL)") 146 | print("=" * 50) 147 | 148 | async with AsyncSessionLocal() as session: 149 | # Создание пользователей 150 | users = [ 151 | User( 152 | username="alice", 153 | email="alice@example.com", 154 | age=25, 155 | balance=Decimal('1000.50') 156 | ), 157 | User( 158 | username="bob", 159 | email="bob@example.com", 160 | age=30, 161 | balance=Decimal('2500.00') 162 | ), 163 | User( 164 | username="charlie", 165 | email="charlie@example.com", 166 | age=28, 167 | balance=Decimal('500.75') 168 | ), 169 | ] 170 | 171 | session.add_all(users) 172 | await session.flush() # Получаем ID без commit 173 | 174 | # Создание постов 175 | posts = [ 176 | Post( 177 | title="Introduction to Async SQLAlchemy", 178 | content="Async operations are great!", 179 | author_id=users[0].id, 180 | published=True, 181 | views=100 182 | ), 183 | Post( 184 | title="PostgreSQL with asyncpg", 185 | content="Fast and efficient!", 186 | author_id=users[0].id, 187 | published=True, 188 | views=250 189 | ), 190 | Post( 191 | title="Python Async Patterns", 192 | content="Using asyncio effectively", 193 | author_id=users[1].id, 194 | published=False 195 | ), 196 | ] 197 | 198 | session.add_all(posts) 199 | await session.flush() 200 | 201 | # Создание комментариев 202 | comments = [ 203 | Comment(content="Great article!", post_id=posts[0].id, author_id=users[1].id), 204 | Comment(content="Very helpful", post_id=posts[0].id, author_id=users[2].id), 205 | Comment(content="Thanks for sharing", post_id=posts[1].id, author_id=users[2].id), 206 | ] 207 | 208 | session.add_all(comments) 209 | await session.commit() 210 | 211 | print(f"✓ Created {len(users)} users") 212 | print(f"✓ Created {len(posts)} posts") 213 | print(f"✓ Created {len(comments)} comments") 214 | 215 | 216 | # ============================================ 217 | # READ операции с оптимизацией 218 | # ============================================ 219 | 220 | async def read_optimized(): 221 | """Оптимизированные запросы с selectinload""" 222 | print("\n" + "=" * 50) 223 | print("READ with Eager Loading (Optimized)") 224 | print("=" * 50) 225 | 226 | async with AsyncSessionLocal() as session: 227 | # Загрузка пользователя с постами и комментариями одним запросом 228 | stmt = ( 229 | select(User) 230 | .options( 231 | selectinload(User.posts).selectinload(Post.comments) 232 | ) 233 | .where(User.username == "alice") 234 | ) 235 | 236 | result = await session.execute(stmt) 237 | alice = result.scalar_one() 238 | 239 | print(f"\nUser: {alice.username} (balance: ${alice.balance})") 240 | print(f"\nPosts ({len(alice.posts)}):") 241 | for post in alice.posts: 242 | print(f" 📝 {post.title}") 243 | print(f" Views: {post.views}, Published: {post.published}") 244 | print(f" Comments: {len(post.comments)}") 245 | for comment in post.comments: 246 | print(f" 💬 {comment.content}") 247 | 248 | 249 | # ============================================ 250 | # Сложные запросы и агрегация 251 | # ============================================ 252 | 253 | async def complex_queries(): 254 | """Сложные запросы с агрегацией""" 255 | print("\n" + "=" * 50) 256 | print("Complex Queries with Aggregation") 257 | print("=" * 50) 258 | 259 | async with AsyncSessionLocal() as session: 260 | # 1. Статистика постов по пользователям 261 | stmt = ( 262 | select( 263 | User.username, 264 | func.count(Post.id).label('post_count'), 265 | func.sum(Post.views).label('total_views'), 266 | func.avg(Post.views).label('avg_views') 267 | ) 268 | .outerjoin(Post) 269 | .group_by(User.username) 270 | .order_by(func.count(Post.id).desc()) 271 | ) 272 | 273 | result = await session.execute(stmt) 274 | stats = result.all() 275 | 276 | print("\n📊 Post statistics:") 277 | for username, post_count, total_views, avg_views in stats: 278 | total = total_views or 0 279 | avg = float(avg_views or 0) 280 | print(f" {username}: {post_count} posts, {total} views (avg: {avg:.1f})") 281 | 282 | # 2. Топ постов по просмотрам 283 | stmt = ( 284 | select(Post) 285 | .options(selectinload(Post.author)) 286 | .where(Post.published == True) 287 | .order_by(Post.views.desc()) 288 | .limit(5) 289 | ) 290 | 291 | result = await session.execute(stmt) 292 | top_posts = result.scalars().all() 293 | 294 | print("\n🏆 Top published posts:") 295 | for i, post in enumerate(top_posts, 1): 296 | print(f" {i}. {post.title} by {post.author.username} ({post.views} views)") 297 | 298 | # 3. Активные пользователи (с опубликованными постами) 299 | stmt = ( 300 | select(User) 301 | .join(Post) 302 | .where(Post.published == True) 303 | .distinct() 304 | ) 305 | 306 | result = await session.execute(stmt) 307 | active_users = result.scalars().all() 308 | 309 | print(f"\n👥 Active users: {[u.username for u in active_users]}") 310 | 311 | 312 | # ============================================ 313 | # Транзакции и изоляция 314 | # ============================================ 315 | 316 | async def transaction_example(): 317 | """Пример работы с транзакциями""" 318 | print("\n" + "=" * 50) 319 | print("Transaction Example") 320 | print("=" * 50) 321 | 322 | async with AsyncSessionLocal() as session: 323 | try: 324 | # Начинаем транзакцию 325 | async with session.begin(): 326 | # Находим пользователя 327 | stmt = select(User).where(User.username == "alice") 328 | result = await session.execute(stmt) 329 | alice = result.scalar_one() 330 | 331 | # Обновляем баланс 332 | old_balance = alice.balance 333 | alice.balance += Decimal('500.00') 334 | 335 | # Создаем пост 336 | new_post = Post( 337 | title="New Transaction Post", 338 | content="Created in transaction", 339 | author_id=alice.id, 340 | published=True 341 | ) 342 | session.add(new_post) 343 | 344 | # Автоматический commit при выходе из блока 345 | print(f"✓ Transaction completed") 346 | print(f" Balance updated: ${old_balance} -> ${alice.balance}") 347 | 348 | except Exception as e: 349 | # Автоматический rollback при ошибке 350 | print(f"✗ Transaction failed: {e}") 351 | 352 | 353 | # ============================================ 354 | # Batch операции 355 | # ============================================ 356 | 357 | async def batch_operations(): 358 | """Пакетные операции для производительности""" 359 | print("\n" + "=" * 50) 360 | print("Batch Operations") 361 | print("=" * 50) 362 | 363 | async with AsyncSessionLocal() as session: 364 | # Batch insert 365 | new_users = [ 366 | User(username=f"user_{i}", email=f"user{i}@example.com", age=20 + i) 367 | for i in range(1, 6) 368 | ] 369 | 370 | session.add_all(new_users) 371 | await session.commit() 372 | 373 | print(f"✓ Batch created {len(new_users)} users") 374 | 375 | # Batch update 376 | stmt = ( 377 | update(User) 378 | .where(User.username.like("user_%")) 379 | .values(is_active=True, balance=Decimal('100.00')) 380 | ) 381 | 382 | result = await session.execute(stmt) 383 | await session.commit() 384 | 385 | print(f"✓ Batch updated {result.rowcount} users") 386 | 387 | # Batch delete 388 | stmt = delete(User).where(User.username.like("user_%")) 389 | result = await session.execute(stmt) 390 | await session.commit() 391 | 392 | print(f"✓ Batch deleted {result.rowcount} users") 393 | 394 | 395 | # ============================================ 396 | # Streaming для больших результатов 397 | # ============================================ 398 | 399 | async def streaming_results(): 400 | """Потоковая обработка для больших наборов данных""" 401 | print("\n" + "=" * 50) 402 | print("Streaming Results") 403 | print("=" * 50) 404 | 405 | async with AsyncSessionLocal() as session: 406 | # Используем stream для обработки больших результатов 407 | stmt = select(User).order_by(User.id) 408 | 409 | result = await session.stream(stmt) 410 | 411 | print("\nStreaming users:") 412 | count = 0 413 | async for (user,) in result: 414 | print(f" Processing: {user.username}") 415 | count += 1 416 | 417 | print(f"✓ Streamed {count} users") 418 | 419 | 420 | # ============================================ 421 | # Main 422 | # ============================================ 423 | 424 | async def init_db(): 425 | """Инициализация базы данных""" 426 | print("Initializing database...") 427 | async with async_engine.begin() as conn: 428 | await conn.run_sync(Base.metadata.drop_all) 429 | await conn.run_sync(Base.metadata.create_all) 430 | print("✓ Database initialized") 431 | 432 | 433 | async def main(): 434 | """Главная функция""" 435 | print("=" * 50) 436 | print("SQLAlchemy 2.0 Async with asyncpg (PostgreSQL)") 437 | print("=" * 50) 438 | 439 | try: 440 | # Инициализация 441 | await init_db() 442 | 443 | # Операции 444 | await create_sample_data() 445 | await read_optimized() 446 | await complex_queries() 447 | await transaction_example() 448 | await batch_operations() 449 | await streaming_results() 450 | 451 | print("\n" + "=" * 50) 452 | print("All operations completed successfully!") 453 | print("=" * 50) 454 | 455 | except Exception as e: 456 | print(f"\n✗ Error: {e}") 457 | import traceback 458 | traceback.print_exc() 459 | 460 | finally: 461 | # Закрытие engine 462 | await async_engine.dispose() 463 | print("\n✓ Database connections closed") 464 | 465 | 466 | if __name__ == "__main__": 467 | # Для запуска требуется: 468 | # pip install sqlalchemy[asyncio] asyncpg 469 | # И запущенный PostgreSQL сервер 470 | asyncio.run(main()) 471 | -------------------------------------------------------------------------------- /example_02/fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример 02: Основные типы полей в SQLAlchemy 2.0 3 | Демонстрирует все основные типы данных с современным синтаксисом 4 | """ 5 | 6 | from sqlalchemy import ( 7 | JSON, create_engine, String, Text, CheckConstraint, Index, 8 | func, select 9 | ) 10 | from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session 11 | from datetime import datetime, date, time, timedelta, timezone 12 | from typing import Any, Optional 13 | from decimal import Decimal 14 | import enum 15 | 16 | 17 | # ============================================ 18 | # SQLAlchemy 2.0 Base 19 | # ============================================ 20 | 21 | class Base(DeclarativeBase): 22 | type_annotation_map = { 23 | dict[str, Any]: JSON # Map dict[str, Any] to the JSON type 24 | } 25 | pass 26 | 27 | 28 | engine = create_engine('sqlite:///fields_example.db', echo=False) 29 | 30 | 31 | # ============================================ 32 | # 1. Числовые типы 33 | # ============================================ 34 | 35 | class Product(Base): 36 | """Пример использования числовых типов""" 37 | __tablename__ = 'products' 38 | 39 | # Primary Key 40 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 41 | 42 | # Целые числа разных размеров 43 | quantity: Mapped[int] = mapped_column(default=0) # SmallInteger в SQLite 44 | stock_count: Mapped[int] = mapped_column(default=0) # Integer 45 | total_views: Mapped[int] = mapped_column(default=0) # BigInteger 46 | 47 | # Числа с плавающей точкой 48 | rating: Mapped[Optional[float]] # Float - приблизительное 49 | price: Mapped[Optional[Decimal]] # Numeric - точное 50 | 51 | # С ограничениями 52 | discount_percent: Mapped[Optional[int]] = mapped_column( 53 | CheckConstraint('discount_percent >= 0 AND discount_percent <= 100') 54 | ) 55 | 56 | 57 | # ============================================ 58 | # 2. Строковые типы 59 | # ============================================ 60 | 61 | class Article(Base): 62 | """Пример использования строковых типов""" 63 | __tablename__ = 'articles' 64 | 65 | id: Mapped[int] = mapped_column(primary_key=True) 66 | 67 | # Строки фиксированной длины 68 | title: Mapped[str] = mapped_column(String(200)) 69 | slug: Mapped[Optional[str]] = mapped_column(String(200), unique=True, index=True) 70 | 71 | # Длинные тексты 72 | content: Mapped[Optional[str]] = mapped_column(Text) 73 | summary: Mapped[Optional[str]] = mapped_column(String(500)) 74 | 75 | # Обязательное поле 76 | author: Mapped[str] = mapped_column(String(100)) 77 | 78 | 79 | # ============================================ 80 | # 3. Дата и время 81 | # ============================================ 82 | 83 | class Event(Base): 84 | """Пример использования типов даты и времени""" 85 | __tablename__ = 'events' 86 | 87 | id: Mapped[int] = mapped_column(primary_key=True) 88 | name: Mapped[Optional[str]] = mapped_column(String(200)) 89 | 90 | # Только дата 91 | event_date: Mapped[Optional[date]] 92 | 93 | # Дата и время 94 | created_at: Mapped[datetime] = mapped_column( 95 | default=lambda: datetime.now(timezone.utc) 96 | ) 97 | updated_at: Mapped[Optional[datetime]] = mapped_column( 98 | onupdate=lambda: datetime.now(timezone.utc) 99 | ) 100 | 101 | # Только время 102 | start_time: Mapped[Optional[time]] 103 | 104 | # Интервал времени 105 | duration: Mapped[Optional[timedelta]] 106 | 107 | # Server-side default (выполняется на стороне БД) 108 | registered_at: Mapped[Optional[datetime]] = mapped_column( 109 | server_default=func.now() 110 | ) 111 | def __repr__(self): 112 | return f"Event(id={self.id}, name={self.name}, event_date={self.event_date}), registered_at={self.registered_at}, created_at={self.created_at}, updated_at={self.updated_at}" 113 | 114 | # ============================================ 115 | # 4. Булевы значения 116 | # ============================================ 117 | 118 | class User(Base): 119 | """Пример использования булевых типов""" 120 | __tablename__ = 'users' 121 | 122 | id: Mapped[int] = mapped_column(primary_key=True) 123 | username: Mapped[str] = mapped_column(String(50), unique=True) 124 | email: Mapped[str] = mapped_column(String(100), unique=True) 125 | 126 | # Булевы флаги 127 | is_active: Mapped[bool] = mapped_column(default=True) 128 | is_admin: Mapped[bool] = mapped_column(default=False) 129 | is_verified: Mapped[bool] = mapped_column(default=False) 130 | email_notifications: Mapped[bool] = mapped_column(default=True) 131 | 132 | 133 | # ============================================ 134 | # 5. JSON 135 | # ============================================ 136 | 137 | class Settings(Base): 138 | """Пример использования JSON типа""" 139 | __tablename__ = 'settings' 140 | 141 | id: Mapped[int] = mapped_column(primary_key=True) 142 | user_id: Mapped[Optional[int]] 143 | 144 | # JSON поле для хранения сложных структур 145 | # В SQLAlchemy 2.0 для JSON можно использовать dict аннотацию 146 | preferences: Mapped[Optional[dict[str, Any]]] 147 | # Поле называется metadata_json, т.к. имя "metadata" зарезервировано в Declarative API 148 | metadata_json: Mapped[Optional[dict[str, Any]]] = mapped_column("metadata") 149 | 150 | 151 | # ============================================ 152 | # 6. Enum 153 | # ============================================ 154 | 155 | class OrderStatus(enum.Enum): 156 | """Enum для статусов заказа""" 157 | PENDING = "pending" 158 | PROCESSING = "processing" 159 | SHIPPED = "shipped" 160 | DELIVERED = "delivered" 161 | CANCELLED = "cancelled" 162 | 163 | 164 | class UserRole(enum.Enum): 165 | """Enum для ролей пользователя""" 166 | ADMIN = "admin" 167 | MODERATOR = "moderator" 168 | USER = "user" 169 | GUEST = "guest" 170 | 171 | 172 | class Order(Base): 173 | """Пример использования Enum типа""" 174 | __tablename__ = 'orders' 175 | 176 | id: Mapped[int] = mapped_column(primary_key=True) 177 | user_id: Mapped[Optional[int]] 178 | total: Mapped[Optional[Decimal]] 179 | 180 | # Enum поля 181 | status: Mapped[OrderStatus] = mapped_column(default=OrderStatus.PENDING) 182 | 183 | 184 | class UserProfile(Base): 185 | """Профиль пользователя с ролью""" 186 | __tablename__ = 'user_profiles' 187 | 188 | id: Mapped[int] = mapped_column(primary_key=True) 189 | username: Mapped[Optional[str]] = mapped_column(String(50)) 190 | role: Mapped[UserRole] = mapped_column(default=UserRole.USER) 191 | 192 | 193 | # ============================================ 194 | # 7. Бинарные данные 195 | # ============================================ 196 | 197 | class File(Base): 198 | """Пример хранения бинарных данных""" 199 | __tablename__ = 'files' 200 | 201 | id: Mapped[int] = mapped_column(primary_key=True) 202 | filename: Mapped[str] = mapped_column(String(255)) 203 | mimetype: Mapped[Optional[str]] = mapped_column(String(100)) 204 | 205 | # Бинарные данные (например, изображения) 206 | content: Mapped[Optional[bytes]] 207 | 208 | uploaded_at: Mapped[datetime] = mapped_column( 209 | default=lambda: datetime.now(timezone.utc) 210 | ) 211 | 212 | 213 | # ============================================ 214 | # 8. Полный пример с ограничениями 215 | # ============================================ 216 | 217 | class Employee(Base): 218 | """Полный пример с различными ограничениями""" 219 | __tablename__ = 'employees' 220 | 221 | # Primary Key с автоинкрементом 222 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 223 | 224 | # NOT NULL (не Optional) 225 | first_name: Mapped[str] = mapped_column(String(50)) 226 | last_name: Mapped[str] = mapped_column(String(50)) 227 | 228 | # UNIQUE 229 | employee_code: Mapped[str] = mapped_column(String(20), unique=True) 230 | email: Mapped[str] = mapped_column(String(100), unique=True) 231 | 232 | # DEFAULT values 233 | department: Mapped[str] = mapped_column(String(50), default='General') 234 | is_active: Mapped[bool] = mapped_column(default=True) 235 | 236 | # CHECK constraints 237 | age: Mapped[Optional[int]] = mapped_column( 238 | CheckConstraint('age >= 18 AND age <= 100') 239 | ) 240 | salary: Mapped[Optional[Decimal]] = mapped_column( 241 | CheckConstraint('salary > 0') 242 | ) 243 | 244 | # INDEX 245 | phone: Mapped[Optional[str]] = mapped_column(String(20), index=True) 246 | 247 | # Timestamps 248 | hired_date: Mapped[date] 249 | created_at: Mapped[datetime] = mapped_column( 250 | default=lambda: datetime.now(timezone.utc) 251 | ) 252 | updated_at: Mapped[Optional[datetime]] = mapped_column( 253 | onupdate=lambda: datetime.now(timezone.utc) 254 | ) 255 | 256 | 257 | # Дополнительные индексы 258 | Index('idx_employee_name', Employee.first_name, Employee.last_name) 259 | 260 | 261 | # ============================================ 262 | # Примеры использования (SQLAlchemy 2.0) 263 | # ============================================ 264 | 265 | def create_examples(): 266 | """Создает примеры записей для всех моделей""" 267 | 268 | # Создаем таблицы 269 | Base.metadata.drop_all(engine) 270 | Base.metadata.create_all(engine) 271 | 272 | with Session(engine) as session: 273 | try: 274 | # 1. Product 275 | print("1. Creating products...") 276 | products = [ 277 | Product( 278 | quantity=100, 279 | stock_count=5000, 280 | total_views=1500000, 281 | rating=4.5, 282 | price=Decimal('99.99'), 283 | discount_percent=10 284 | ), 285 | Product( 286 | quantity=50, 287 | stock_count=2000, 288 | total_views=500000, 289 | rating=4.8, 290 | price=Decimal('149.99'), 291 | discount_percent=15 292 | ) 293 | ] 294 | session.add_all(products) 295 | 296 | # 2. Article 297 | print("2. Creating articles...") 298 | article = Article( 299 | title="Introduction to SQLAlchemy 2.0", 300 | slug="intro-sqlalchemy-20", 301 | content="SQLAlchemy 2.0 is a powerful ORM for Python...", 302 | summary="Learn the basics of SQLAlchemy 2.0", 303 | author="John Doe" 304 | ) 305 | session.add(article) 306 | 307 | # 3. Event 308 | print("3. Creating events...") 309 | event = Event( 310 | name="Python Conference 2024", 311 | event_date=date(2024, 6, 15), 312 | start_time=time(9, 0, 0), 313 | duration=timedelta(hours=8) 314 | ) 315 | session.add(event) 316 | 317 | # 4. User 318 | print("4. Creating users...") 319 | users = [ 320 | User( 321 | username="admin", 322 | email="admin@example.com", 323 | is_active=True, 324 | is_admin=True, 325 | is_verified=True 326 | ), 327 | User( 328 | username="john_doe", 329 | email="john@example.com", 330 | is_active=True, 331 | is_admin=False, 332 | is_verified=True 333 | ) 334 | ] 335 | session.add_all(users) 336 | 337 | # 5. Settings 338 | print("5. Creating settings...") 339 | settings = Settings( 340 | user_id=1, 341 | preferences={ 342 | "theme": "dark", 343 | "language": "ru", 344 | "notifications": { 345 | "email": True, 346 | "push": False, 347 | "sms": False 348 | }, 349 | "privacy": { 350 | "profile_visible": True, 351 | "show_email": False 352 | } 353 | }, 354 | metadata_json={ 355 | "last_login": "2024-01-15T10:30:00", 356 | "login_count": 42, 357 | "favorite_features": ["dark_mode", "shortcuts"] 358 | } 359 | ) 360 | session.add(settings) 361 | 362 | # 6. Order 363 | print("6. Creating orders...") 364 | orders = [ 365 | Order(user_id=1, total=Decimal('299.99'), status=OrderStatus.DELIVERED), 366 | Order(user_id=2, total=Decimal('149.99'), status=OrderStatus.PROCESSING), 367 | Order(user_id=1, total=Decimal('99.99'), status=OrderStatus.PENDING) 368 | ] 369 | session.add_all(orders) 370 | 371 | # 7. UserProfile 372 | print("7. Creating user profiles...") 373 | profiles = [ 374 | UserProfile(username="admin", role=UserRole.ADMIN), 375 | UserProfile(username="moderator1", role=UserRole.MODERATOR), 376 | UserProfile(username="user1", role=UserRole.USER) 377 | ] 378 | session.add_all(profiles) 379 | 380 | # 8. File 381 | print("8. Creating file...") 382 | file_content = b"This is binary content of a file" 383 | file = File( 384 | filename="document.txt", 385 | mimetype="text/plain", 386 | content=file_content 387 | ) 388 | session.add(file) 389 | 390 | # 9. Employee 391 | print("9. Creating employees...") 392 | employees = [ 393 | Employee( 394 | first_name="Alice", 395 | last_name="Smith", 396 | employee_code="EMP001", 397 | email="alice@company.com", 398 | department="Engineering", 399 | age=30, 400 | salary=Decimal('75000.00'), 401 | phone="+1234567890", 402 | hired_date=date(2020, 1, 15) 403 | ), 404 | Employee( 405 | first_name="Bob", 406 | last_name="Johnson", 407 | employee_code="EMP002", 408 | email="bob@company.com", 409 | department="Marketing", 410 | age=28, 411 | salary=Decimal('65000.00'), 412 | phone="+1234567891", 413 | hired_date=date(2021, 3, 20) 414 | ) 415 | ] 416 | session.add_all(employees) 417 | 418 | # Commit всех изменений 419 | session.commit() 420 | print("\n✓ All records created successfully!") 421 | 422 | # Вывод примеров (SQLAlchemy 2.0 style) 423 | print("\n" + "=" * 50) 424 | print("Examples of stored data:") 425 | print("=" * 50) 426 | # Product 427 | stmt = select(Product) 428 | product = session.execute(stmt).scalars().first() 429 | print(f"\nProduct: rating={product.rating}, price={product.price}") 430 | 431 | # Article 432 | stmt = select(Article) 433 | article = session.execute(stmt).scalars().first() 434 | print(f"\nArticle: {article.title} by {article.author}") 435 | 436 | # Event 437 | stmt = select(Event) 438 | event: Event | None = session.execute(stmt).scalars().first() 439 | print(f"\nEvent: {event}") 440 | 441 | # User 442 | stmt = select(User).where(User.is_admin == True) 443 | user = session.execute(stmt).scalars().first() 444 | print(f"\nAdmin User: {user.username} ({user.email})") 445 | 446 | # Settings (JSON) 447 | stmt = select(Settings) 448 | settings = session.execute(stmt).scalars().first() 449 | print(f"\nSettings theme: {settings.preferences['theme']}") 450 | print(f"Email notifications: {settings.preferences['notifications']['email']}") 451 | 452 | # Order (Enum) 453 | stmt = select(Order) 454 | order = session.execute(stmt).scalars().first() 455 | print(f"\nOrder: #{order.id}, status={order.status.value}, total=${order.total}") 456 | 457 | # UserProfile (Enum) 458 | stmt = select(UserProfile).where(UserProfile.role == UserRole.ADMIN) 459 | profile = session.execute(stmt).scalars().first() 460 | print(f"\nProfile: {profile.username} - {profile.role.value}") 461 | 462 | # Employee 463 | stmt = select(Employee) 464 | employee = session.execute(stmt).scalars().first() 465 | print(f"\nEmployee: {employee.first_name} {employee.last_name}, " 466 | f"salary=${employee.salary}, age={employee.age}") 467 | 468 | except Exception as e: 469 | session.rollback() 470 | print(f"Error: {e}") 471 | raise 472 | 473 | 474 | # ============================================ 475 | # Main 476 | # ============================================ 477 | 478 | if __name__ == "__main__": 479 | print("=" * 50) 480 | print("SQLAlchemy 2.0 Field Types Example") 481 | print("=" * 50) 482 | 483 | create_examples() 484 | 485 | print("\n" + "=" * 50) 486 | print("Database created: fields_example.db") 487 | print("You can inspect it using SQLite browser") 488 | print("=" * 50) 489 | --------------------------------------------------------------------------------