Skip to content

Commit 3549fe9

Browse files
начата работа с бд
1 parent 153aedd commit 3549fe9

21 files changed

+1523
-17
lines changed

README.md

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,23 @@
1-
# Работа с базами данных
1+
# Fastapi-Django
2+
3+
Приложение, разрабатываемое при помощи это библиотеки: [https://github.com/albertalexandrov/fastapi-django-example](https://github.com/albertalexandrov/fastapi-django-example)
4+
5+
Вспомогательная библиотека для разработки приложений на FastAPI.
6+
7+
Реализует функционал для наиболее распространных ситуаций (создание экзмепляра приложения FastAPI по заданным настройкам,
8+
работа с БД, фильтрация, пагинация, авторизация, аутентификация).
9+
10+
- [Названия библиотеки](#названия-библиотеки)
11+
- [TODO](#todo)
12+
- [Создание приложения](#создание-приложения)
13+
- [Запуск приложения](#запуск-приложения)
14+
- [Работа с БД](#работа-с-бд)
15+
- [Сессии SQLAlchemy](#сессии-sqlalchemy)
16+
- [Миграции](#миграции)
17+
- [Исследование имеющихся решений](#исследование-имеющихся-решений)
18+
- [fastapi-sqla](#fastapi-sqla)
19+
- [repository-sqlalchemy](#repository-sqlalchemy)
20+
- [FastAPI-SQLAlchemy](#fastapi-sqlalchemy)
221

322
## Названия библиотеки
423

@@ -75,6 +94,54 @@ python manage.py runserver
7594

7695
Это запустит экземпляр указанного в UVICORN_APP приложения при помощи uvicorn.
7796

97+
## Работа с БД
98+
99+
### Сессии SQLAlchemy
100+
101+
Работа с базами данных происходит через слой репозиториев. Для этого разработан [базовый класс репозитория BaseRepository](fastapi_django/db/repositories/base.py#L13),
102+
который предоставляет возможность работать с данными в стиле Django ORM:
103+
104+
```python
105+
from fastapi_django.db.repositories.base import BaseRepository
106+
from fastapi_django.db.sessions import contextified_autocommit_session
107+
108+
109+
class UsersRepository(BaseRepository):
110+
model_cls = User
111+
112+
113+
async with contextified_autocommit_session():
114+
repository = UsersRepository()
115+
users = await repository.objects.filter(name="Иван").all()
116+
```
117+
118+
Обратите внимание, что сессия SQLAlchemy не передается при инициализации репозитория. Вместо этого она инициализируется
119+
контекстным менеджером contextified_autocommit_session() и помещается в ContextVars. Репозитории (все в пределах действия
120+
контекстного менеджера) затем берут инициализированную сессию оттуда. contextified_autocommit_session() также управляет
121+
жизненным циклом сессии.
122+
123+
Настройки базы данных задаются в settings(.py) в DATABASE. Пример:
124+
125+
```python
126+
DATABASE = {
127+
"DRIVERNAME": "postgresql+asyncpg",
128+
"DATABASE": "fastapi-django",
129+
"USERNAME": "postgres",
130+
"PASSWORD": "postgres",
131+
"HOST": "127.0.0.1",
132+
"PORT": "5433",
133+
"OPTIONS": {
134+
"echo": True
135+
},
136+
}
137+
```
138+
139+
где OPTIONS - необязательные аргументы, которые будут переданы как kwargs в функцию create_async_engine().
140+
141+
### Миграции
142+
143+
Работа с миграциями остается привычной - через консольную команду alembic.
144+
78145
## Исследование имеющихся решений
79146

80147
[https://github.com/mjhea0/awesome-fastapi](https://github.com/mjhea0/awesome-fastapi)

fastapi_django/conf/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def _setup(self, name=None):
1919
This is used the first time settings are needed, if the user hasn't
2020
configured settings manually.
2121
"""
22-
settings_module = os.environ.get(ENVIRONMENT_VARIABLE)
22+
settings_module = os.environ.get(ENVIRONMENT_VARIABLE, "settings")
2323
if not settings_module:
2424
raise ValueError("не сконфигурировано")
2525

fastapi_django/conf/global_settings.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,22 @@
1414

1515
TRUSTED_HOST_MIDDLEWARE_ALLOWED_HOSTS = ["*"]
1616

17-
# DATABASES = {}
17+
DATABASE: dict = {}
18+
# пример:
19+
# DATABASE: dict = {
20+
# "DRIVERNAME": "postgresql+asyncpg",
21+
# "USERNAME": "username",
22+
# "PASSWORD": "password",
23+
# "HOST": "localhost",
24+
# "PORT": 5432,
25+
# "DATABASE": "database",
26+
# "OPTIONS": {
27+
# "connect_args": {"timeout": 30},
28+
# "echo": True,
29+
# "pool_recycle": 3600,
30+
# # другие параметры, которые будут переданы как kw в функцию create_async_engine()
31+
# }
32+
# }
1833

1934
# UVICORN_APP определяется непосредственно в настройках приложения,
2035
# тк библиотека предоставляет только функцию get_default_app, которая создает экзмепляр приложения

fastapi_django/db/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from sqlalchemy import URL
2+
from sqlalchemy.ext.asyncio import create_async_engine
3+
4+
from fastapi_django.conf import settings
5+
from fastapi_django.exceptions import ImproperlyConfigured
6+
7+
8+
# поддерживаемые диалекты SQLAlchemy https://docs.sqlalchemy.org/en/20/dialects/index.html
9+
# SQLAlchemy как безоговорочный вариант для взаимодействия с БД
10+
11+
# TODO:
12+
# по умолчанию не устанавливать библиотеку SQLAlchemy
13+
# проверять факт установки SQLAlchemy
14+
15+
16+
class EngineProxy:
17+
def __init__(self):
18+
if not settings.DATABASE:
19+
raise ImproperlyConfigured("База данных не сконфигурирована")
20+
# TODO: прочекать параметры для разных диалектов
21+
url = URL.create(
22+
drivername=settings.DATABASE["DRIVERNAME"],
23+
username=settings.DATABASE.get("USERNAME"),
24+
password=settings.DATABASE.get("PASSWORD"),
25+
host=settings.DATABASE.get("HOST"),
26+
port=settings.DATABASE.get("PORT"),
27+
database=settings.DATABASE["DATABASE"],
28+
)
29+
kw = settings.DATABASE.get("OPTIONS", {})
30+
self.__dict__["_engine"] = create_async_engine(url, **kw) # иначе будет RecursionError: maximum recursion depth exceeded
31+
32+
def __getattr__(self, item):
33+
return getattr(self._engine, item)
34+
35+
def __setattr__(self, name, value):
36+
return setattr(self._engine, name, value)
37+
38+
def __delattr__(self, name):
39+
return delattr(self._engine, name)
40+
41+
42+
engine = EngineProxy()

fastapi_django/db/dependencies.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import Any, AsyncIterator, Callable, Annotated
2+
3+
from fastapi import Depends
4+
from sqlalchemy.ext.asyncio import AsyncSession
5+
6+
from fastapi_django.db.sessions import contextified_transactional_session, contextified_autocommit_session
7+
8+
9+
def contextify_transactional_session(**kw: Any) -> Callable:
10+
async def wrapper() -> AsyncIterator[AsyncSession]:
11+
async with contextified_transactional_session(**kw) as session:
12+
yield session
13+
return wrapper
14+
15+
16+
def contextify_autocommit_session(**kw: Any) -> Callable:
17+
async def wrapper() -> AsyncIterator[AsyncSession]:
18+
async with contextified_autocommit_session(**kw) as session:
19+
yield session
20+
return wrapper
21+
22+
23+
ContextifiedTransactionalSession = Annotated[AsyncSession, Depends(contextify_transactional_session())]
24+
ContextifiedAutocommitSession = Annotated[AsyncSession, Depends(contextify_autocommit_session())]

fastapi_django/db/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class ColumnNotFoundError(Exception):
2+
def __init__(self, model, column_name: str):
3+
error = (
4+
f"Столбец `{column_name}` не найден в модели {model.__name__}"
5+
)
6+
super().__init__(error)

fastapi_django/db/models/__init__.py

Whitespace-only changes.

fastapi_django/db/models/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from typing import Self
2+
3+
from sqlalchemy import MetaData
4+
from sqlalchemy.ext.asyncio import AsyncAttrs
5+
from sqlalchemy.orm import DeclarativeBase
6+
7+
metadata = MetaData()
8+
9+
10+
class Model(AsyncAttrs, DeclarativeBase):
11+
metadata = metadata
12+
13+
def update(self, **values) -> Self:
14+
for key, value in values.items():
15+
setattr(self, key, value)
16+
return self
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from fastapi_django.db.repositories.base import BaseRepository
2+
3+
__all__ = ["BaseRepository"]
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import logging
2+
from itertools import islice
3+
from typing import Generic, Any, Type, Self
4+
5+
from fastapi_django.db.repositories.queryset import QuerySet
6+
from fastapi_django.db.sessions import session_context_var
7+
from fastapi_django.db.types import Model
8+
from fastapi_django.exceptions import ImproperlyConfigured
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class BaseRepository(Generic[Model]):
14+
model_cls: Type[Model]
15+
16+
def __init__(self):
17+
if not self.model_cls:
18+
raise ImproperlyConfigured(f"Не задана модель в атрибуте `{self.__class__.__name__}.model_cls`")
19+
self._session = session_context_var.get()
20+
assert self._session is not None, "Сессия не определена. Используйте декоратор"
21+
print(self._session)
22+
self._flush = False
23+
self._commit = False
24+
print(f"Инициировали репозиторий {self.__class__.__name__}")
25+
26+
def _clone(self) -> Self:
27+
clone = self.__class__()
28+
clone._flush = self._flush
29+
clone._commit = self._commit
30+
return clone
31+
32+
def flush(self, flush: bool = True, /) -> Self:
33+
clone = self._clone()
34+
clone._flush = flush
35+
return clone
36+
37+
def commit(self, commit: bool = True, /) -> Self:
38+
clone = self._clone()
39+
clone._commit = commit
40+
return clone
41+
42+
async def _flush_commit_reset(self, *objs: Model) -> None:
43+
if self._flush and not self._commit and objs:
44+
await self._session.flush(objs)
45+
elif self._commit:
46+
await self._session.commit()
47+
self._flush = False
48+
self._commit = False
49+
50+
async def create(self, **kw: Any) -> Model:
51+
obj = self.model_cls(**kw)
52+
self._session.add(obj)
53+
await self._flush_commit_reset(obj)
54+
return obj
55+
56+
async def bulk_create(self, values: list[dict], batch_size: int | None = None) -> list[Model]:
57+
if batch_size is not None and (not isinstance(batch_size, int) or batch_size <= 0):
58+
raise ValueError("batch_size должен быть целым положительным числом")
59+
objs = []
60+
if batch_size:
61+
it = iter(values)
62+
while batch := list(islice(it, batch_size)):
63+
batch_objs = [self.model_cls(**item) for item in batch]
64+
await self._flush_commit_reset(*batch_objs)
65+
objs.extend(batch_objs)
66+
else:
67+
for item in values:
68+
obj = self.model_cls(**item)
69+
objs.append(obj)
70+
await self._flush_commit_reset(*objs)
71+
return objs
72+
73+
async def get_by_pk(self, pk: Any) -> Model | None:
74+
return await self._session.get(self.model_cls, pk)
75+
76+
@property
77+
def objects(self) -> QuerySet:
78+
return QuerySet(self.model_cls, self._session)

0 commit comments

Comments
 (0)