Skip to content

Commit 10d1e07

Browse files
authored
Merge pull request #29 from igorbenav/more-efficient-query
More efficient select
2 parents 9387ecb + e538102 commit 10d1e07

File tree

6 files changed

+406
-113
lines changed

6 files changed

+406
-113
lines changed

README.md

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
- Easy redis caching
4848
- Easy client-side caching
4949
- ARQ integration for task queue
50+
- Efficient querying (only queries what's needed)
5051
- Easily extendable
5152
- Flexible
5253
- Easy running with docker compose
@@ -63,6 +64,7 @@
6364
#### Features
6465
- [ ] Add a Rate Limiter decorator
6566
- [ ] Add mongoDB support
67+
- [ ] Add support in schema_to_select for dict as well as Pydantic Schema
6668

6769
#### Security
6870
- [x] FastAPI docs behind authentication and hidden based on the environment
@@ -403,12 +405,19 @@ First, you may want to take a look at the project structure and understand what
403405
└── src # Source code directory.
404406
├── __init__.py # Initialization file for the src package.
405407
├── alembic.ini # Configuration file for Alembic (database migration tool).
408+
├── poetry.lock
409+
├── pyproject.toml # Configuration file for Poetry, lists project dependencies.
410+
406411
├── app # Main application directory.
407412
│ ├── __init__.py # Initialization file for the app package.
413+
│ ├── main.py # Entry point that imports and creates the FastAPI application instance.
414+
│ ├── worker.py # Worker script for handling background tasks.
415+
│ │
408416
│ ├── api # Folder containing API-related logic.
409417
│ │ ├── __init__.py
410418
│ │ ├── dependencies.py # Defines dependencies that can be reused across the API endpoints.
411419
│ │ ├── exceptions.py # Contains custom exceptions for the API.
420+
│ │ │
412421
│ │ └── v1 # Version 1 of the API.
413422
│ │ ├── __init__.py
414423
│ │ ├── login.py # API routes related to user login.
@@ -431,37 +440,38 @@ First, you may want to take a look at the project structure and understand what
431440
│ │ ├── __init__.py
432441
│ │ ├── crud_base.py # Base CRUD operations class that can be extended by other CRUD modules.
433442
│ │ ├── crud_posts.py # CRUD operations for posts.
434-
│ │ └── crud_users.py # CRUD operations for users.
443+
│ │ ├── crud_users.py # CRUD operations for users.
444+
│ │ └── helper.py # Helper functions for CRUD operations.
435445
│ │
436-
│ ├── main.py # Entry point that imports and creates the FastAPI application instance.
437446
│ ├── models # ORM models for the application.
438447
│ │ ├── __init__.py
439448
│ │ ├── post.py # ORM model for posts.
440449
│ │ └── user.py # ORM model for users.
441450
│ │
442-
│ ├── schemas # Pydantic schemas for data validation.
443-
│ │ ├── __init__.py
444-
│ │ ├── job.py # Schemas related to background jobs.
445-
│ │ ├── post.py # Schemas related to posts.
446-
│ │ └── user.py # Schemas related to users.
447-
│ │
448-
│ └── worker.py # Worker script for handling background tasks.
451+
│ └── schemas # Pydantic schemas for data validation.
452+
│ ├── __init__.py
453+
│ ├── job.py # Schemas related to background jobs.
454+
│ ├── post.py # Schemas related to posts.
455+
│ └── user.py # Schemas related to users.
449456
450457
├── migrations # Directory for Alembic migrations.
451458
│ ├── README # General info and guidelines for migrations.
452459
│ ├── env.py # Environment configurations for Alembic.
453460
│ ├── script.py.mako # Template script for migration generation.
461+
│ │
454462
│ └── versions # Folder containing individual migration scripts.
463+
│ └── README.MD
455464
456-
├── pyproject.toml # Configuration file for Poetry, lists project dependencies.
457465
├── scripts # Utility scripts for the project.
466+
│ ├── __init__.py
458467
│ └── create_first_superuser.py # Script to create the first superuser in the application.
459468
460469
└── tests # Directory containing all the tests.
461470
├── __init__.py # Initialization file for the tests package.
462471
├── conftest.py # Configuration and fixtures for pytest.
463472
├── helper.py # Helper functions for writing tests.
464473
└── test_user.py # Tests related to the user model and endpoints.
474+
465475
```
466476

467477
### 5.2 Database Model
@@ -552,6 +562,89 @@ CRUDEntity = CRUDBase[Entity, EntityCreateInternal, EntityUpdate, EntityUpdateIn
552562
crud_entity = CRUDEntity(Entity)
553563
```
554564

565+
So, for users:
566+
```python
567+
# crud_users.py
568+
from app.model.user import User
569+
from app.schemas.user import UserCreateInternal, UserUpdate, UserUpdateInternal, UserDelete
570+
571+
CRUDUser = CRUDBase[User, UserCreateInternal, UserUpdate, UserUpdateInternal, UserDelete]
572+
crud_users = CRUDUser(User)
573+
```
574+
575+
When actually using the crud in an endpoint, to get data you just pass the database connection and the attributes as kwargs:
576+
```python
577+
# Here I'm getting the users with email == user.email
578+
user = await crud_users.get(db=db, email=user.email)
579+
```
580+
581+
To get a list of objects with the attributes, you should use the get_multi:
582+
```python
583+
# Here I'm getting 100 users with the name David except for the first 3
584+
user = await crud_users.get_multi(
585+
db=db,
586+
offset=3,
587+
limit=100,
588+
name="David"
589+
)
590+
```
591+
592+
To create, you pass a `CreateSchemaType` object with the attributes, such as a `UserCreate` pydantic schema:
593+
```python
594+
from app.core.schemas.user import UserCreate
595+
596+
# Creating the object
597+
user_internal = UserCreate(
598+
name="user",
599+
username="myusername",
600+
601+
)
602+
603+
# Passing the object to be created
604+
crud_users.create(db=db, object=user_internal)
605+
```
606+
607+
To just check if there is at least one row that matches a certain set of attributes, you should use `exists`
608+
```python
609+
# This queries only the email variable
610+
# It returns True if there's at least one or False if there is none
611+
crud_users.exists(db=db, email=user@example.com)
612+
```
613+
614+
To update you pass an `object` which may be a `pydantic schema` or just a regular `dict`, and the kwargs.
615+
You will update with `objects` the rows that match your `kwargs`.
616+
```python
617+
# Here I'm updating the user with username == "myusername".
618+
# #I'll change his name to "Updated Name"
619+
crud_users.update(db=db, object={name="Updated Name"}, username="myusername")
620+
```
621+
622+
To delete we have two options:
623+
- db_delete: actually deletes the row from the database
624+
- delete:
625+
- adds `"is_deleted": True` and `deleted_at: datetime.utcnow()` if the model inherits from `PersistentDeletion` (performs a soft delete), but keeps the object in the database.
626+
- actually deletes the row from the database if the model does not inherit from `PersistentDeletion`
627+
628+
```python
629+
# Here I'll just change is_deleted to True
630+
crud_users.delete(db=db, username="myusername")
631+
632+
# Here I actually delete it from the database
633+
crud_users.db_delete(db=db, username="myusername")
634+
```
635+
636+
#### More Efficient Selecting
637+
For the `get` and `get_multi` methods we have the option to define a `schema_to_select` attribute, which is what actually makes the queries more efficient. When you pass a pydantic schema in `schema_to_select` to the `get` or `get_multi` methods, only the attributes in the schema will be selected.
638+
```python
639+
from app.schemas.user import UserRead
640+
# Here it's selecting all of the user's data
641+
crud_user.get(db=db, username="myusername")
642+
643+
# Now it's only selecting the data that is in UserRead.
644+
# Since that's my response_model, it's all I need
645+
crud_user.get(db=db, username="myusername", schema_to_select=UserRead)
646+
```
647+
555648
### 5.7 Routes
556649
Inside `app/api/v1`, create a new `entities.py` file and create the desired routes
557650
```python

src/app/api/v1/posts.py

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async def write_post(
2323
current_user: Annotated[UserRead, Depends(get_current_user)],
2424
db: Annotated[AsyncSession, Depends(async_get_db)]
2525
):
26-
db_user = await crud_users.get(db=db, username=username, is_deleted=False)
26+
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
2727
if db_user is None:
2828
raise HTTPException(status_code=404, detail="User not found")
2929

@@ -32,6 +32,7 @@ async def write_post(
3232

3333
post_internal_dict = post.model_dump()
3434
post_internal_dict["created_by_user_id"] = db_user.id
35+
3536
post_internal = PostCreateInternal(**post_internal_dict)
3637
return await crud_posts.create(db=db, object=post_internal)
3738

@@ -43,11 +44,11 @@ async def read_posts(
4344
username: str,
4445
db: Annotated[AsyncSession, Depends(async_get_db)]
4546
):
46-
db_user = await crud_users.get(db=db, username=username, is_deleted=False)
47+
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
4748
if db_user is None:
4849
raise HTTPException(status_code=404, detail="User not found")
4950

50-
posts = await crud_posts.get_multi(db=db, created_by_user_id=db_user.id, is_deleted=False)
51+
posts = await crud_posts.get_multi(db=db, schema_to_select=PostRead, created_by_user_id=db_user.id, is_deleted=False)
5152
return posts
5253

5354

@@ -59,18 +60,18 @@ async def read_post(
5960
id: int,
6061
db: Annotated[AsyncSession, Depends(async_get_db)]
6162
):
62-
db_user = await crud_users.get(db=db, username=username, is_deleted=False)
63+
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
6364
if db_user is None:
6465
raise HTTPException(status_code=404, detail="User not found")
6566

66-
db_post = await crud_posts.get(db=db, id=id, created_by_user_id=db_user.id, is_deleted=False)
67+
db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, created_by_user_id=db_user.id, is_deleted=False)
6768
if db_post is None:
6869
raise HTTPException(status_code=404, detail="Post not found")
6970

7071
return db_post
7172

7273

73-
@router.patch("/{username}/post/{id}", response_model=PostRead)
74+
@router.patch("/{username}/post/{id}")
7475
@cache(
7576
"{username}_post_cache",
7677
resource_id_name="id",
@@ -84,18 +85,19 @@ async def patch_post(
8485
current_user: Annotated[UserRead, Depends(get_current_user)],
8586
db: Annotated[AsyncSession, Depends(async_get_db)]
8687
):
87-
db_user = await crud_users.get(db=db, username=username, is_deleted=False)
88+
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
8889
if db_user is None:
8990
raise HTTPException(status_code=404, detail="User not found")
9091

9192
if current_user.id != db_user.id:
9293
raise privileges_exception
9394

94-
db_post = await crud_posts.get(db=db, id=id, is_deleted=False)
95+
db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False)
9596
if db_post is None:
9697
raise HTTPException(status_code=404, detail="Post not found")
9798

98-
return await crud_posts.update(db=db, object=values, db_object=db_post, id=id)
99+
await crud_posts.update(db=db, object=values, id=id)
100+
return {"message": "Post updated"}
99101

100102

101103
@router.delete("/{username}/post/{id}")
@@ -111,24 +113,20 @@ async def erase_post(
111113
current_user: Annotated[UserRead, Depends(get_current_user)],
112114
db: Annotated[AsyncSession, Depends(async_get_db)]
113115
):
114-
db_user = await crud_users.get(db=db, username=username, is_deleted=False)
116+
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
115117
if db_user is None:
116118
raise HTTPException(status_code=404, detail="User not found")
117119

118120
if current_user.id != db_user.id:
119121
raise privileges_exception
120122

121-
db_post = await crud_posts.get(db=db, id=id, is_deleted=False)
123+
db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False)
122124
if db_post is None:
123125
raise HTTPException(status_code=404, detail="Post not found")
124126

125-
deleted_post = await crud_posts.delete(db=db, db_object=db_post, id=id)
126-
if deleted_post.is_deleted == True:
127-
message = {"message": "Post deleted"}
128-
else:
129-
message = {"message": "Something went wrong"}
130-
131-
return message
127+
await crud_posts.delete(db=db, db_row=db_post, id=id)
128+
129+
return {"message": "Post deleted"}
132130

133131

134132
@router.delete("/{username}/db_post/{id}", dependencies=[Depends(get_current_superuser)])
@@ -143,20 +141,13 @@ async def erase_db_post(
143141
id: int,
144142
db: Annotated[AsyncSession, Depends(async_get_db)]
145143
):
146-
db_user = await crud_users.get(db=db, username=username, is_deleted=False)
144+
db_user = await crud_users.get(db=db, schema_to_select=UserRead, username=username, is_deleted=False)
147145
if db_user is None:
148146
raise HTTPException(status_code=404, detail="User not found")
149147

150-
db_post = await crud_posts.get(db=db, id=id, is_deleted=False)
148+
db_post = await crud_posts.get(db=db, schema_to_select=PostRead, id=id, is_deleted=False)
151149
if db_post is None:
152150
raise HTTPException(status_code=404, detail="Post not found")
153151

154152
await crud_posts.db_delete(db=db, db_object=db_post, id=id)
155-
deleted_post = await crud_posts.get(db=db, id=id)
156-
157-
if deleted_post is None:
158-
message = {"message": "Post deleted"}
159-
else:
160-
message = {"message": "Something went wrong"}
161-
162-
return message
153+
return {"message": "Post deleted from the database"}

0 commit comments

Comments
 (0)