-
-
Notifications
You must be signed in to change notification settings - Fork 6.6k
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to do flexibly use nested pydantic models for sqlalchemy ORM #2194
Comments
Have you tried @tiangolo's Pydantic SQLAlchemy ? |
Thank you for the contribution, but your proposal is for converting sqlalchemy model to pydantic, I need a way to convert a nested model from pydantic to sqlalchemy. |
I am thinking about a recursive function to do what I want. I currently need an external "maping" dict that translates the pydantic sub class to the related sqlalchemy class by their name. Would be helpful if someone could provide an example of a recursive function for two dicts. Added difficulty that sub model could be lists or direct keys. |
@j-gimbel I'll play with this more but this is what I came up with. # Too lazy to delete unused imports :(
from typing import Optional, Union, List, Dict, Any, Mapping, Type, TypeVar, Generic
def pydantic_to_sqlalchemy(schema: pydantic.main.ModelMetaclass) -> Any:
__fields_dict__ = {}
def recurse(obj: pydantic.main.ModelMetaclass, temp_key: str = "") -> None:
if isinstance(obj, pydantic.main.ModelMetaclass):
for key, value in obj.schema().items():
recurse(obj=value, temp_key=temp_key + key if temp_key else key)
if isinstance(obj, dict):
for key, value in obj.items():
recurse(obj=value, temp_key=temp_key + key if temp_key else key)
if isinstance(obj, list):
for item in range(len(obj)):
recurse(
obj=obj[item],
temp_key=temp_key + str(item) if temp_key else str(item),
)
else:
__fields_dict__[temp_key] = obj
recurse(schema)
return __fields_dict__ Not sure how this would be helpful to you, maybe just for inspiration. But I'll keep playing with this. Out: {'': <class '__main__.SchemaRoot'>,
'definitions': {'SchemaSubBase': {'properties': {'someSubText': {'title': 'Somesubtext',
'type': 'string'}},
'required': ['someSubText'],
'title': 'SchemaSubBase',
'type': 'object'}},
'definitionsSchemaSubBase': {'properties': {'someSubText': {'title': 'Somesubtext',
'type': 'string'}},
'required': ['someSubText'],
'title': 'SchemaSubBase',
'type': 'object'},
'definitionsSchemaSubBaseproperties': {'someSubText': {'title': 'Somesubtext',
'type': 'string'}},
'definitionsSchemaSubBasepropertiessomeSubText': {'title': 'Somesubtext',
'type': 'string'},
'definitionsSchemaSubBasepropertiessomeSubTexttitle': 'Somesubtext',
'definitionsSchemaSubBasepropertiessomeSubTexttype': 'string',
'definitionsSchemaSubBaserequired0': 'someSubText',
'definitionsSchemaSubBasetitle': 'SchemaSubBase',
'definitionsSchemaSubBasetype': 'object',
'properties': {'id': {'title': 'Id', 'type': 'integer'},
'someRootText': {'title': 'Someroottext', 'type': 'string'},
'subData': {'default': [],
'items': {'$ref': '#/definitions/SchemaSubBase'},
'title': 'Subdata',
'type': 'array'}},
'propertiesid': {'title': 'Id', 'type': 'integer'},
'propertiesidtitle': 'Id',
'propertiesidtype': 'integer',
'propertiessomeRootText': {'title': 'Someroottext', 'type': 'string'},
'propertiessomeRootTexttitle': 'Someroottext',
'propertiessomeRootTexttype': 'string',
'propertiessubData': {'default': [],
'items': {'$ref': '#/definitions/SchemaSubBase'},
'title': 'Subdata',
'type': 'array'},
'propertiessubDataitems': {'$ref': '#/definitions/SchemaSubBase'},
'propertiessubDataitems$ref': '#/definitions/SchemaSubBase',
'propertiessubDatatitle': 'Subdata',
'propertiessubDatatype': 'array',
'required0': 'someRootText',
'required1': 'id',
'title': 'SchemaRoot',
'type': 'object'} |
Tortoise has some stuff with pydantic conversion. |
Thank you both, I will check this out tomorrow. |
Hey @j-gimbel ! Have you managed to find a solution? 👀 |
Hey @bazakoskon, maybe you have managed to find a solution? 👀 |
Hello, I reused the "_declarative_constructor" function code (which is the by-default init method for SQLAlchemy Base class)
I guess the overrided "_declarative_constructor" function can be generalized (and used as "contructor" parameter when calling declarative_base()) to handle automatically iterables found in the list of kwargs, based on the class relationships. EDIT:
|
I did check this for nested object and it doesnt work |
Would be thankful if u can add some full example,i tried to implement it and couldn't make it work ,thanks |
Did u manage to find for this a solution? |
@avico78
Please note that the proposed implementation requires that nested models are in the same module as parent models, as you can see here:
But this can be easily modified if needed |
@scd75 - great work man! I've tested with ~3-4 level of json level depth and it work perfectly, |
@scd75 - found some issue ,
And by the pyadntic class we define the relation as one-->one
meaning the json would look like:
It failed with :
Maybe im wrong , but seems it doesn't cover the case of nested dictionaries? |
see more Info, Basically, simple relation just adding "uselist=False"
I have customer table I expect each customer insreted with a single address
File "/usr/local/lib/python3.8/site-packages/sqlalchemy/orm/state.py", line 434, in _initialize_instance
|
Hi @avico78 , it is expected to not work with one-to-one relationships, as I only added the case of one-to-many relationships in the following code for init:
I guess you can easily add here a handling of "ONETOONE" relationships (and by the way I cleaned a little bit the code):
let me know if that works.. But the question is: are you sure that want to "Create" a new object for the corresponding one-to-one relationship? (this was my use case for one-to-many, but just make sure that you also want to create the target to the one-to-one. By 'create' I mean, not link to an existing object) |
@scd75 , thanks much for your reply ,
first level objects(flat ones) - has a single occurrence So the schema should be :
Pydanmtic :
Now , sub_table1 in SchemaMainBase define as List as it can multiple occurrences,
So to create such object we can either map it from main table (he has single occurrence )
those mapping rules can be define by the user so it open for manipluation Sadly , I lost by mistake the code I start over writing based your new Base function . Thanks again ! my email avico78@gmail.com |
@scd75 , after loosing my whole code folder I just finish coding the whole thing again .
I guess it may related to they i setup the scripts /Base location. base_class.py
base.py
Now im taking the Base from base.py
Running the endpoint with proper input,
Tables being drop/create successfully from main :
You mention :
I did tried placing the function and class also in module - same result . |
@avico78 , understand it is an issue with the finding of the related model name. what does it look like? PS: maybe would be good to carry on this discussion on another thread to not pollute the initial one |
@scd75 ,thanks
Test:
debug prints:
Not sure what is it mean? it didnt return the related object(subscriber as string) but as object? |
@scd75 , "if we actually made this a feature, then it would be a magnet for new user requests and issues that would be better solved if they just made their own constructor that does what they want. I'm still struggling making it work,
following your suggestion I added debug print: from sqlalchemy.ext.declarative import declarative_base
import sys
def _declarative_constructor_auto_instantiate_nested(self, **kwargs):
cls_ = type(self)
relationships = self.__mapper__.relationships
for k in kwargs:
if not hasattr(cls_, k):
raise TypeError(
"%r is an invalid keyword argument for %s" % (k, cls_.__name__)
)
if k in relationships.keys():
if relationships[k].direction.name == 'ONETOMANY':
print("Current Key:",k,"relationships_direction:",relationships[k].direction.name,"argument:",relationships[k].argument)
#childclass = getattr(sys.modules[self.__module__], relationships[k].argument)
#setattr(self, k, [childclass(**elem) for elem in kwargs[k]])
else:
setattr(self, k, kwargs[k])
_declarative_constructor_auto_instantiate_nested.__name__ = "__init__"
Base = declarative_base(
constructor=_declarative_constructor_auto_instantiate_nested)
router (i commented some part to see the print of relationships[k].argument): def db_add_nested_data_pydantic_generic(db: Session, root: SchemaCustomerBase):
# this fails:
db_root = CustomerModel(**root.dict())
# db.add(db_root)
# db.commit()
# db.refresh(db_root)
return db_root
@app_sa_router.post("/addNestedModel_pydantic_generic")
def add_nested_data_pydantic_generic(root: SchemaCustomerBase, db: Session = Depends(get_db)):
print("im root",root)
data = db_add_nested_data_pydantic_generic(db=db, root=root)
return {"ok":1} And I get #coming from router
im root customer_no=0 first_name='string' last_name='string' subscriber=[SchemaSubscriberBase(subscriber_no=0)]
#coming from function
Current Key: subscriber relationships_direction: ONETOMANY argument: <bound method _class_resolver._resolve_name of <sqlalchemy.ext.declarative.clsregistry._class_resolver object at 0x7f060942dbe0>> Any idea? |
@avico78 @scd75 Last few days I have created a library for this, mostly because I wanted to learn how to do that. It seems I went with a different approach. Please let me know what you think, feedback is greatly appriciated. Github: https://github.com/Wouterkoorn/sqlalchemy-pydantic-orm |
@Wouterkoorn - tnx for your update i surely check it, |
Hi 👋🏻 I ended up arriving to this issue when looking for ways to enable nested model auto-instantiation in my SQLAlchemy projects. My comment here is not strictly related to FastAPI, but could improve the solution provided in @scd75 's comment. My proposal includes two major changes:
In order to define our custom constructor as a common Proposed constructor: from sqlalchemy.orm import ONETOMANY
from sqlalchemy.orm import declarative_base
Base = declarative_base(constructor=None)
class BaseModel:
"""Base class for all the Python data models"""
def __init__(self, **kwargs):
"""
Custom initializer that allows nested children initialization.
Only keys that are present as instance's class attributes are allowed.
These could be, for example, any mapped columns or relationships.
Code inspired from GitHub.
Ref: https://github.com/tiangolo/fastapi/issues/2194
"""
cls = self.__class__
model_columns = self.__mapper__.columns
relationships = self.__mapper__.relationships
for key, val in kwargs.items():
if not hasattr(cls, key):
raise TypeError(f"Invalid keyword argument: {key}")
if key in model_columns:
setattr(self, key, val)
continue
if key in relationships:
relation_dir = relationships[key].direction.name
relation_cls = relationships[key].mapper.entity
if relation_dir == ONETOMANY.name:
instances = [relation_cls(**elem) for elem in val]
setattr(self, key, instances)
class ChildModel(Base, BaseModel):
...
class ParentModel(Base, BaseModel):
... |
Hi @Sinclert , thanks for your comment. as in here:
|
Hey @scd75 , I believe the is no technical difference. However, defining the constructor as you would normally do with any other |
Not sure If I am wrong.
And I always get a Also, the official document said: Within the ORM, “one-to-one” is considered as a convention where the ORM expects that only one related row will exist for any parent row. So I guess ONETOONE is also regarded as ONETOMANY in Sqlachemy ? And I slightly change the code using
|
@yinzixie I think you are right. There is no In my initial testing, all the For instance, I noticed that the relationship constructor receives an argument called |
@tiangolo Can we sponsor a fix to this issue or have you opine here? This issue is perhaps the messiest part of this whole FastApi framework. We have SqlAlchemy -> Pydantic models working very nicely, but going the other direction is involved -- especially when you throw in relationships and composite primary keys. We have a modification of https://github.com/Wouterkoorn/sqlalchemy-pydantic-orm in the works to see if that will solve our issue (adding non "id" pk support, and adding composite pks). |
Agree with @don4of4. I tried @Wouterkoorn solution and it works great with nested models. @tiangolo please take a look on this if you can. |
You're also welcome to make a PR. I'll be happy to spend some more time on it as well, it was indeed just a first POC version (although we do use it in production) |
I used __ init__ method in the BaseModel class to convert the nested Dictonary into an ORM object.
|
This issue was moved to a discussion.
You can continue the conversation there. Go to discussion →
First check
Example
Description
My Question is:
How to make nested sqlalchemy models from nested pydantic models (or python dicts) in a generic way and write them to the datase in "one shot".
My example model is called "root model" and has a list of submodels called "sub models" in "subData" key.
Please see above for pydantic and sql alchemy definitions.
Example:
The user provides a nested json string:
Open the browser and call the endpoint
/docs
.You can play around with all endpoints and POST the json string from above.
/addNestedModel_pydantic_generic
When you call the endpoint /addNestedModel_pydantic_generic it will fail, because sqlalchemy cannot create the nested model from pydantic nested model directly:
AttributeError: 'dict' object has no attribute '_sa_instance_state'
/addSimpleModel_pydantic
With a non-nested model it works.
The remaining endpoints are showing "hacks" to solve the problem of nested models.
/addNestedModel_pydantic
In this endpoint is generate the root model and andd the submodels with a loop in a non-generic way with pydantic models.
/addNestedModel_pydantic
In this endpoint is generate the root model and andd the submodels with a loop in a non-generic way with python dicts.
My solutions are only hacks, I want a generic way to create nested sqlalchemy models either from pydantic (preferred) or from a python dict.
Environment
The text was updated successfully, but these errors were encountered: