FastAPI Черепаха ORM Пользователи FastAPI (Python) — Отношения — Многие Ко Многим

#python #sqlite #foreign-keys #fastapi #tortoise-orm

Вопрос:

Я использую пользователей FastAPI, Tortoise ORM и FastAPI для создания API для обучения. В основном я получаю данные о погоде в городах из API OpenWeather и храню их в базе данных SQLite с помощью Tortoise ORM. У меня есть аутентификация с пользователями FastAPI.

Я использовал внешний ключ, и отношения работали нормально. Но теперь я хочу кое-что изменить (улучшить). Я хочу, чтобы у каждого пользователя были свои элементы, и после входа в систему и доступа к конечной точке ( @router.get("/me/onlyitems/", response_model=List[schemas.Card_Pydantic]) ) он получает только их элементы.

Это работало нормально, я получаю только пользовательские элементы (используя owner_id), но в моей таблице элементов используется внешний уникальный идентификатор (элемент-город, и у каждого города есть уникальный идентификатор), и это первичный ключ. Но когда другой пользователь пытается добавить тот же город, он выдает ошибку, потому что в таблице не может быть того же города, даже с другим владельцем. Что ж, так и должно быть… но я хочу, чтобы другие пользователи получали доступ к тем же городам (у них та же информация, они не настроены). Затем я хочу, чтобы у многих пользователей был доступ к одному и тому же элементу (и каждый пользователь может получить доступ к нескольким элементам).

Я не думаю, что идеально повторять информацию об элементе (используя другой идентификатор) для каждого пользователя, было бы лучше, если бы разные пользователи получали доступ к одному и тому же элементу.

Я думаю, что это было бы отношение «Многие ко многим», но я не знаю, как установить отношения и получить их с помощью FastAPI.

Я думал о том, чтобы создать промежуточную таблицу только с идентификаторами пользователя и города (и идентификатором автоинкремента) и использовать ее для получения связанных данных, но я не знаю, лучший ли это способ или это возможно.

Я попытался изучить документацию Tortoise, но не смог найти нужную мне часть или понять некоторые сложные примеры и части документации.

Я использую обновленные версии пакетов и Python 3.9.4

Что ж, позвольте мне показать кое-какой код:

models.py

 class UserModel(TortoiseBaseUserModel):  # From FastAPI Users
    cards: fields.ReverseRelation["Card"]


class OAuthAccountModel(TortoiseBaseOAuthAccountModel): # From FastAPI Users
    user = fields.ForeignKeyField("models.UserModel", related_name="oauth_accounts")


class User(models.BaseUser, models.BaseOAuthAccountMixin): # From FastAPI Users
    pass


class UserCreate(models.BaseUserCreate): # From FastAPI Users
    pass


class UserUpdate(User, models.BaseUserUpdate): # From FastAPI Users
    pass


class UserDB(User, models.BaseUserDB, PydanticModel): # From FastAPI Users
    class Config:
        orm_mode = True
        orig_model = UserModel


user_db = TortoiseUserDatabase(UserDB, UserModel, OAuthAccountModel) # From FastAPI Users


class Card(tmodels.Model):
    """
    The Card model, related to the user.
    """
    id = fields.IntField(pk=True)
    city_name = fields.CharField(max_length=30)
    temperature = fields.FloatField()
    state = fields.CharField(max_length=30, null=True)
    icon = fields.CharField(max_length=3, null=True)
    period = fields.CharField(max_length=20, null=True)
    created_at = fields.DatetimeField(auto_now_add=True)
    updated_at = fields.DatetimeField(auto_now=True)
    owner: fields.ForeignKeyRelation[UserModel] = fields.ForeignKeyField(
        "models.UserModel", related_name="cards")

    class PydanticMeta:
        exclude = ["owner"]
 

I tried to change my owner field and the their relationship to UserModel:

 class Card(tmodels.Model):
owner = fields.ManyToManyField("models.UserModel", related_name="cards")
 
 class UserModel(TortoiseBaseUserModel):
    cards: fields.ManyToManyRelation["Card"]
 

I don’t know if it is right, I couldn’t make it works, but the problem could be in other places.

My endpoint for the user create a item:

 @router.post("/", response_model=Card_Pydantic)
async def create_item_for_current_user(
        card: CardCreate, user: UserDB = Depends(fastapi_users.get_current_active_user)):
    card_obj = await crud.create_user_card(card=card, user=user)
    if card_obj is None:
        raise HTTPException(status_code=404, detail=f"City '{card.city_name}' not found")
    return await card_obj
 

The CRUD function used (to create an item) here:

 async def create_user_card(
        card: schemas.CardCreate, user: UserDB = Depends(fastapi_users.get_current_active_user)):
    card_json = await weather_api.get_api_info_by_name(card.city_name)
    if card_json is None:
        return None
    card_obj = await Card.create(**card_json,
                                 owner_id=user.id)  # ** is used to pass the keys values as kwargs.
    return await card_obj
 

Моя конечная точка для получения элементов, принадлежащих пользователю:

 @router.get("/me/onlyitems/", response_model=List[schemas.Card_Pydantic])
async def read_current_user_items(user: UserDB = Depends(fastapi_users.get_current_active_user)):
    user_cards = await schemas.Card_Pydantic.from_queryset(Card.filter(owner_id=user.id).all())
    return await crud.update_only_card_for(user_cards, user.id)
 

Моя функция crud-это:

 async def update_only_card_for(user_cards, owner_id):
    for city in user_cards:
        await update_card(city.id, owner_id)
    return await schemas.Card_Pydantic.from_queryset(Card.filter(owner_id=owner_id).all())

# The update function (it will check if it need to update or not)
async def update_card(city_id: int, owner_id: UUID = UserDB):
    card_obj = await Card.get(id=city_id)
    card_time = card_obj.updated_at
    timezone = card_time.tzinfo
    now = datetime.now(timezone)
    time_to_update = now - timedelta(minutes=15)

    if card_time < time_to_update:
        card_json = await weather_api.get_api_info_by_id(city_id)
        await card_obj.update_from_dict(card_json).save()  # with save will update the update_at field
    card_updated = await schemas.Card_Pydantic.from_queryset_single(Card.get(id=city_id))
    return card_updated
 

Как я мог бы изменить его, чтобы получить доступ к картам, необходимым пользователю? Я думаю, что не могу использовать фильтр по идентификатору владельца (ну, если бы я мог создать свою промежуточную таблицу, я мог бы, но не знаю, как это сделать и получить данные de), но не могу придумать, как связать пользователя с добавленными им элементами, и в то же время другие пользователи получают доступ к тем же элементам.

Этот код работает нормально (я использую nuxt.js передний конец), пока не появится дублированный город…

Как лучше всего с этим справиться и как?

Спасибо, Диего.

Ответ №1:

Я решил эту проблему. Я думаю, что документация Tortoise немного запутана, но я нашел, как создавать взаимосвязи и извлекать данные.

Модели: (упрощенная версия)

 class Card(tmodels.Model):
    id = fields.IntField(pk=True)
    city_name = fields.CharField(max_length=30)
    owners: fields.ManyToManyRelation["UserModel"] = fields.ManyToManyField(
        "models.UserModel", related_name="cards", through="card_usermodel")

class UserModel(TortoiseBaseUserModel):
    cards: fields.ManyToManyRelation[Card]
 

through="card_usermodel" создаст промежуточную таблицу с идентификатором карты и идентификатором владельца, она определит взаимосвязь.

Для выполнения операций с данными:

 async def create_user_card(
        card: schemas.CardCreate, user: UserDB = Depends(fastapi_users.get_current_active_user)):
    card_json = await weather_api.get_api_info_by_name(card.city_name)
    if card_json is None:
        return None
    owner = await UserModel.get_or_none(id=user.id)
    card_obj = await Card.update_or_create(**card_json)
    await card_obj[0].owners.add(owner)
    return await schemas.Card_Response_Pydantic.from_queryset_single(Card.get(id=card_json.get("id"), owners=user.id))
 

Это основное. Я также изменил некоторые модели Pydantic, чтобы проверить ответы.