Как гибко использовать вложенные питантические модели для sqlalchemy

from fastapi import Depends, FastAPI, HTTPException, Body, Request
from sqlalchemy import create_engine, Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session, sessionmaker, relationship
from sqlalchemy.inspection import inspect
from typing import List, Optional
from pydantic import BaseModel
import json

SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
app = FastAPI()


# sqlalchemy models

class RootModel(Base):
    __tablename__ = "root_table"
    id = Column(Integer, primary_key=True, index=True)
    someRootText = Column(String)
    subData = relationship("SubModel", back_populates="rootData")


class SubModel(Base):
    __tablename__ = "sub_table"
    id = Column(Integer, primary_key=True, index=True)
    someSubText = Column(String)
    root_id = Column(Integer, ForeignKey("root_table.id"))
    rootData = relationship("RootModel", back_populates="subData")


# pydantic models/schemas
class SchemaSubBase(BaseModel):
    someSubText: str

    class Config:
        orm_mode = True


class SchemaSub(SchemaSubBase):
    id: int
    root_id: int

    class Config:
        orm_mode = True


class SchemaRootBase(BaseModel):
    someRootText: str
    subData: List[SchemaSubBase] = []

    class Config:
        orm_mode = True


class SchemaRoot(SchemaRootBase):
    id: int

    class Config:
        orm_mode = True


class SchemaSimpleBase(BaseModel):
    someRootText: str

    class Config:
        orm_mode = True


class SchemaSimple(SchemaSimpleBase):
    id: int

    class Config:
        orm_mode = True


Base.metadata.create_all(bind=engine)


# database functions (CRUD)

def db_add_simple_data_pydantic(db: Session, root: SchemaRootBase):
    db_root = RootModel(**root.dict())
    db.add(db_root)
    db.commit()
    db.refresh(db_root)
    return db_root


def db_add_nested_data_pydantic_generic(db: Session, root: SchemaRootBase):

    # this fails:
    db_root = RootModel(**root.dict())
    db.add(db_root)
    db.commit()
    db.refresh(db_root)
    return db_root


def db_add_nested_data_pydantic(db: Session, root: SchemaRootBase):

    # start: hack: i have to manually generate the sqlalchemy model from the pydantic model
    root_dict = root.dict()
    sub_dicts = []

    # i have to remove the list form root dict in order to fix the error from above
    for key in list(root_dict):
        if isinstance(root_dict[key], list):
            sub_dicts = root_dict[key]
            del root_dict[key]

    # now i can do it
    db_root = RootModel(**root_dict)
    for sub_dict in sub_dicts:
        db_root.subData.append(SubModel(**sub_dict))

    # end: hack
    db.add(db_root)
    db.commit()
    db.refresh(db_root)
    return db_root


def db_add_nested_data_nopydantic(db: Session, root):
    print(root)
    sub_dicts = root.pop("subData")
    print(sub_dicts)
    db_root = RootModel(**root)

    for sub_dict in sub_dicts:
        db_root.subData.append(SubModel(**sub_dict))
    db.add(db_root)
    db.commit()
    db.refresh(db_root)

    # problem
    """
    if I would now "return db_root", the answer would be of this:
    {
        "someRootText": "string",
        "id": 24
    }

    and not containing "subData"
    therefore I have to do the following.
    Why?

    """
    from sqlalchemy.orm import joinedload

    db_root = (
        db.query(RootModel)
            .options(joinedload(RootModel.subData))
            .filter(RootModel.id == db_root.id)
            .all()
    )[0]
    return db_root


# Dependency
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


@app.post("/addNestedModel_pydantic_generic", response_model=SchemaRootBase)
def addSipleModel_pydantic_generic(root: SchemaRootBase, db: Session = Depends(get_db)):
    data = db_add_simple_data_pydantic(db=db, root=root)
    return data


@app.post("/addSimpleModel_pydantic", response_model=SchemaSimpleBase)
def add_simple_data_pydantic(root: SchemaSimpleBase, db: Session = Depends(get_db)):
    data = db_add_simple_data_pydantic(db=db, root=root)
    return data


@app.post("/addNestedModel_nopydantic")
def add_nested_data_nopydantic(root=Body(...), db: Session = Depends(get_db)):
    data = db_add_nested_data_nopydantic(db=db, root=root)
    return data


@app.post("/addNestedModel_pydantic", response_model=SchemaRootBase)
def add_nested_data_pydantic(root: SchemaRootBase, db: Session = Depends(get_db)):
    data = db_add_nested_data_pydantic(db=db, root=root)
    return data

Описание

Мой вопрос:

Как сделать вложенные модели sqlalchemy из вложенных моделей pydantic (или Python dicts) обычным способом и записать их в базу данных одним выстрелом.

Моя примерная модель называется RootModel и имеет список подмоделей, называемых подмоделями в ключе subData.

См. Определения pydantic и sqlalchemy выше.

Пример: пользователь предоставляет вложенную строку json:

{
  "someRootText": "string",
  "subData": [
    {
      "someSubText": "string"
    }
  ]
}

Откройте браузер и вызовите конечную точку /docs. Вы можете поиграть со всеми конечными точками и выполнить POST строку json сверху.

/addNestedModel_pydantic_generic

При вызове конечной точки / addNestedModel_pydantic_generic произойдет сбой, поскольку sqlalchemy не может напрямую создать вложенную модель из вложенной модели pydantic: AttributeError: 'dict' object has no attribute '_sa_instance_state'

​/addSimpleModel_pydantic

С невложенной моделью все работает.

Остальные конечные точки демонстрируют хаки для решения проблемы вложенных моделей.

/addNestedModel_pydantic

В этой конечной точке генерируется корневая модель и обрабатываются подмодели с помощью цикла, не являющегося универсальным, с использованием пидантических моделей.

/addNestedModel_pydantic

В этой конечной точке создается корневая модель и обрабатываются подмодели с помощью цикла, не являющегося универсальным, с помощью Python dicts.

Мои решения - это всего лишь хаки, мне нужен общий способ создания вложенных моделей sqlalchemy либо из pydantic (предпочтительно), либо из Python dict.

Среда

  • ОС: Windows,
  • Версия FastAPI: 0.61.1
  • Версия Python: Python 3.8.5
  • sqlalchemy: 1.3.19
  • pydantic: 1.6.1

person j-gimbel    schedule 18.10.2020    source источник
comment
Вы нашли решение?   -  person AviC    schedule 10.04.2021


Ответы (2)


Я не нашел хорошего встроенного способа сделать это в pydantic / SQLAlchemy. Как я это решил: я дал каждой вложенной пидантической модели класс Meta, содержащий соответствующую модель SQLAlchemy. Вот так:

from pydantic import BaseModel
from models import ChildDBModel, ParentDBModel

class ChildModel(BaseModel):
    some_attribute: str = 'value'
    class Meta:
        orm_model = ChildDBModel

class ParentModel(BaseModel):
    child: SubModel

Это позволило мне написать универсальную функцию, которая проходит через pydantic объект и преобразует подмодели в модели SQLAlchemy:

def is_pydantic(obj: object):
    """Checks whether an object is pydantic."""
    return type(obj).__class__.__name__ == "ModelMetaclass"


def parse_pydantic_schema(schema):
    """
        Iterates through pydantic schema and parses nested schemas
        to a dictionary containing SQLAlchemy models.
        Only works if nested schemas have specified the Meta.orm_model.
    """
    parsed_schema = dict(schema)
    for key, value in parsed_schema.items():
        try:
            if isinstance(value, list) and len(value):
                if is_pydantic(value[0]):
                    parsed_schema[key] = [schema.Meta.orm_model(**schema.dict()) for schema in value]
            else:
                if is_pydantic(value):
                    parsed_schema[key] = value.Meta.orm_model(**value.dict())
        except AttributeError:
            raise AttributeError("Found nested Pydantic model but Meta.orm_model was not specified.")
    return parsed_schema

Функция parse_pydantic_schema возвращает словарное представление пидантической модели, где подмодели заменяются соответствующей моделью SQLAlchemy, указанной в Meta.orm_model. Вы можете использовать это возвращаемое значение для создания родительской модели SQLAlchemy за один раз:

parsed_schema = parse_pydantic_schema(parent_model)  # parent_model is an instance of pydantic ParentModel 
new_db_model = ParentDBModel(**parsed_schema)
# do your db actions/commit here

Если вы хотите, вы можете даже расширить это, чтобы также автоматически создать родительскую модель, но для этого вам также потребуется указать Meta.orm_model для всех pydantic моделей.

person Daan Beverdam    schedule 30.04.2021

Хорошая функция @dann, для более чем двух уровней вложенности вы можете использовать эту рекурсивную функцию:


def pydantic_to_sqlalchemy_model(schema):
    """
    Iterates through pydantic schema and parses nested schemas
    to a dictionary containing SQLAlchemy models.
    Only works if nested schemas have specified the Meta.orm_model.
    """
    parsed_schema = dict(schema)
    for key, value in parsed_schema.items():
        try:
            if isinstance(value, list) and len(value) and is_pydantic(value[0]):
                parsed_schema[key] = [
                    item.Meta.orm_model(**pydantic_to_sqlalchemy_model(item))
                    for item in value
                ]
            elif is_pydantic(value):
                parsed_schema[key] = value.Meta.orm_model(
                    **pydantic_to_sqlalchemy_model(value)
                )
        except AttributeError:
            raise AttributeError(
                f"Found nested Pydantic model in {schema.__class__} but Meta.orm_model was not specified."
            )
    return parsed_schema

Используйте его экономно! если у вас циклическое вложение, оно будет повторяться вечно.

А затем назовите вас преобразователем данных вот так:

def create_parent(db: Session, parent: Parent_pydantic_schema):
    db_parent = Parent_model(**pydantic_to_sqlalchemy_model(intent))
    db.add(db_parent)
    db.commit()
    return db_parent
person Viki    schedule 29.06.2021