Skip to content

Commit d9b5e5f

Browse files
committed
[sqlalchemy] allow create/update with object for one/many 2 many
For one-2-many and many-2-many relationships, allow the create and update routes to accept a partial object in the foreign key attribute. For example: client.post("/heros", json={ "name": Bob, "team": {"name": "Avengers"} } Assuming there is already a team called Avengers, Bob will be created, the Team with name "Avengers" will be looked up and used to populate Bob's team_id foreign key attribute. The only setup required is for the input model for the foreign object to specify the Table class that can be used to lookup the object. For example: class Team(Base): """Team DTO.""" __tablename__ = "teams" id = Column(Integer, primary_key=True, index=True) name = Column(String, unique=True) class TeamUpdate(Model): name: str class Meta: orm_model = Team
1 parent 66fd32f commit d9b5e5f

File tree

2 files changed

+363
-4
lines changed

2 files changed

+363
-4
lines changed

fastapi_crudrouter/core/sqlalchemy.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Any, Callable, List, Type, Generator, Optional, Union
2+
from collections.abc import Sequence
23

34
from fastapi import Depends, HTTPException
45

@@ -9,10 +10,12 @@
910
from sqlalchemy.orm import Session
1011
from sqlalchemy.ext.declarative import DeclarativeMeta as Model
1112
from sqlalchemy.exc import IntegrityError
13+
from sqlalchemy import column
1214
except ImportError:
1315
Model = None
1416
Session = None
1517
IntegrityError = None
18+
column = None
1619
sqlalchemy_installed = False
1720
else:
1821
sqlalchemy_installed = True
@@ -97,13 +100,53 @@ def route(
97100

98101
return route
99102

103+
104+
def _get_orm_object(self, db: Session, orm_model: Model, model: Model) -> Any:
105+
query = db.query(orm_model)
106+
filter_items = 0
107+
for key, val in model.dict().items():
108+
if val:
109+
filter_items += 1
110+
query = query.filter(column(key) == val)
111+
if filter_items == 0:
112+
raise Exception("No attributes for filter found")
113+
return query.one()
114+
115+
116+
def _get_orm_object_or_value(self, db: Session, val: Any) -> Any:
117+
"""Return an inflated database object or a plain value.
118+
119+
If a `val` is a SqlModel type and has defined a Meta.orm model
120+
attribute, lookup the object from the `db` and return it.
121+
Otherwise, just return the `val`. If `val` is a sequence of
122+
objects, return the sequence of objects from the db.
123+
"""
124+
# we want to iterate through sequences but not strings
125+
if not val or isinstance(val, str):
126+
return val
127+
128+
if isinstance(val, Sequence):
129+
return [self._get_orm_object_or_value(db, v) for v in val]
130+
else:
131+
if meta_class := getattr(val, "Meta", None):
132+
if orm_model := getattr(meta_class, "orm_model", None):
133+
return self._get_orm_object(db, orm_model, val)
134+
return val
135+
136+
100137
def _create(self, *args: Any, **kwargs: Any) -> CALLABLE:
138+
101139
def route(
102140
model: self.create_schema, # type: ignore
103141
db: Session = Depends(self.db_func),
104142
) -> Model:
105143
try:
106-
db_model: Model = self.db_model(**model.dict())
144+
db_model: Model = self.db_model()
145+
146+
for key, val in model:
147+
if val:
148+
setattr(db_model, key, self._get_orm_object_or_value(db, val))
149+
107150
db.add(db_model)
108151
db.commit()
109152
db.refresh(db_model)
@@ -123,9 +166,10 @@ def route(
123166
try:
124167
db_model: Model = self._get_one()(item_id, db)
125168

126-
for key, value in model.dict(exclude={self._pk}).items():
127-
if hasattr(db_model, key):
128-
setattr(db_model, key, value)
169+
for key, val in model:
170+
if key != self._pk:
171+
if hasattr(db_model, key):
172+
setattr(db_model, key, self._get_orm_object_or_value(db, val))
129173

130174
db.commit()
131175
db.refresh(db_model)

0 commit comments

Comments
 (0)