Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
246 views
in Technique[技术] by (71.8m points)

python - Model Relationships not showing in Tortoise-ORM + FastAPI

I was playing around with FastAPI using Tortoise-ORM for it's orm and encountered a problem. Specifically, I cannot return a relationship in the model.

Here is my application structure. Structure is inspired by Django's app structure.

.
├── Dockerfile
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── app
│   ├── contacts
│   │   ├── __init__.py
│   │   ├── main.py
│   │   ├── models.py
│   │   └── routers.py
│   ├── main.py
│   ├── resources
│   │   ├── __init__.py
│   │   ├── constants.py
│   │   ├── core_model.py
│   │   ├── database.py
│   │   └── middlewares.py
│   └── users
│       ├── __init__.py
│       ├── main.py
│       ├── models.py
│       └── routers.py
└── docker-compose.yml

database connection is setup in app/resources/database.py like so;

from fastapi import FastAPI
from tortoise.contrib.fastapi import register_tortoise


def get_db_uri(*, user, password, host, db):
    return f'postgres://{user}:{password}@{host}:5432/{db}'


def setup_database(app: FastAPI):
    register_tortoise(
        app,
        db_url=get_db_uri(
            user='postgres',
            password='postgres',
            host='db',  # docker-compose service name
            db='postgres',
        ),
        modules={
            'models': [
                'app.users.models',
                'app.contacts.models',
            ],
        },
        generate_schemas=True,
        add_exception_handlers=True,
    )

from the models argument you can see that there are 2 models setup. Here are the two.

app/users/models.py

from tortoise import Tortoise, fields
from tortoise.contrib.pydantic import pydantic_model_creator
from passlib.hash import bcrypt

from app.resources.core_model import CoreModel


class User(CoreModel):
    username = fields.CharField(50, unique=True)
    email = fields.CharField(60, unique=True)
    password_hash = fields.CharField(128)

    def __str__(self):
        return self.email

    def verify_password(self, password):
        return bcrypt.verify(password, self.password_hash)

    class PydanticMeta:
        exclude = ["password_hash"]


# Tortoise.init_models(['app.users.models'], 'models')

User_Pydantic = pydantic_model_creator(User, name='User')
UserIn_Pydantic = pydantic_model_creator(
    User, name='UserIn', exclude_readonly=True)

app/contacts/models.py

from tortoise import Tortoise, fields
from tortoise.contrib.pydantic import pydantic_model_creator

from app.resources.core_model import CoreModel


class Contact(CoreModel):
    user = fields.ForeignKeyField(
        'models.User', related_name='contacts')
    name = fields.CharField(50)

    def __str__(self):
        return self.name


# Tortoise.init_models(['app.users'], 'models')


Contact_Pydantic = pydantic_model_creator(Contact, name='Contact')
ContactIn_Pydantic = pydantic_model_creator(
    Contact, name='ContactIn', exclude_readonly=True)

Here is what happens when the user tries to save a contact.

@router.post('/', status_code=status.HTTP_201_CREATED)
async def create_contact(contact_name: str = Form(...), user: User_Pydantic = Depends(get_current_user)):
    try:
        contact = Contact(user_id=user.id, name=contact_name)
        await contact.save()
        contact_obj = await Contact_Pydantic.from_tortoise_orm(contact)
        print(contact_obj.schema_json(indent=4))
    except Exception as err:
        print(err)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail='failed to save data')
    return {'status_code': status.HTTP_201_CREATED, 'contact': contact_obj.dict()}

users can retrieve their saved contacts from the following route.

@router.get('/me')
async def get_all_contacts(user: User_Pydantic = Depends(get_current_user)):
    try:
        contacts = await Contact.filter(user_id=user.id)
        contacts_list = [await Contact_Pydantic.from_tortoise_orm(contact) for contact in contacts]
        print(user.schema_json())
    except:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail='failed to fetch related data')
    return {'contacts': contacts_list}

when the user retrieves their contacts it does not show the relationship with the users. In this example it is not necessary but I would like to figure out how to get the relationships in the response for future reference.

I went through the docs and found that early-init is a thing and tried to use init_models but this does not seem to work. Or maybe I just don't know how it works. If I need to use init_models, confusing part is the 1. when to call it and 2. how to call it. init_models two arguments made me very confused.

For summary, I have 2 questions.

  1. How can I get the relationship from a model and return that to users.
  2. If I need to use init_models, where do I call it and with this application structure what will be the correct way for the 2 required arguments.

Thank you in advance.

Note

  1. I have tried the following method to save the contact but the result was the same.
@router.post('/', status_code=status.HTTP_201_CREATED)
async def create_contact(contact_name: str = Form(...), user: User_Pydantic = Depends(get_current_user)):
    try:
        user = await User.get(id=user.id)
        contact = await Contact.create(user=user, name=contact_name)
        contact_obj = await Contact_Pydantic.from_tortoise_orm(contact)
        print(contact_obj.schema_json(indent=4))
    except Exception as err:
        print(err)
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST, detail='failed to save data')
    return {'status_code': status.HTTP_201_CREATED, 'contact': contact_obj.dict()}
question from:https://stackoverflow.com/questions/65879512/model-relationships-not-showing-in-tortoise-orm-fastapi

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Thanks to @alex_noname I could achieve this.

First, separate the pydantic schemas from the models.py.

.
├── Dockerfile
├── LICENSE
├── Pipfile
├── Pipfile.lock
├── README.md
├── app
│   ├── contacts
│   │   ├── __init__.py
│   │   ├── main.py
│   │   ├── models.py
│   │   ├── routers.py
│   │   └── schemas.py  <- new
│   ├── main.py
│   ├── resources
│   │   ├── __init__.py
│   │   ├── constants.py
│   │   ├── core_model.py
│   │   ├── database.py
│   │   └── middlewares.py
│   └── users
│       ├── __init__.py
│       ├── main.py
│       ├── models.py
│       ├── routers.py
│       └── schemas.py  <- new
└── docker-compose.yml

app/users/schemas.py

from tortoise.contrib.pydantic import pydantic_model_creator

from .models import User


User_Pydantic = pydantic_model_creator(User, name='User')
UserIn_Pydantic = pydantic_model_creator(
    User, name='UserIn', exclude_readonly=True)

app/contacts/schemas.py

from tortoise.contrib.pydantic import pydantic_model_creator

from .models import Contact


Contact_Pydantic = pydantic_model_creator(Contact, name='Contact')
ContactIn_Pydantic = pydantic_model_creator(
    Contact, name='ContactIn', exclude_readonly=True)

Then, in app/resources/database.py where we are setting up the tortoise-orm, call init_models.

from fastapi import FastAPI
from tortoise import Tortoise
from tortoise.contrib.fastapi import register_tortoise


def get_db_uri(*, user, password, host, db):
    return f'postgres://{user}:{password}@{host}:5432/{db}'


def setup_database(app: FastAPI):
    register_tortoise(
        app,
        db_url=get_db_uri(
            user='postgres',
            password='postgres',
            host='db',  # docker-composeのservice名
            db='postgres',
        ),
        modules={
            'models': [
                'app.users.models',
                'app.contacts.models',
            ],
        },
        # modules={"models": ["_models_"]},
        generate_schemas=True,
        add_exception_handlers=True,
    )


Tortoise.init_models(['app.users.models', 'app.contacts.models'], 'models')  # <- added

and that's it!
This worked like a charm.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...