BeanieODM / beanie

Asynchronous Python ODM for MongoDB
http://beanie-odm.dev/
Apache License 2.0
2.04k stars 215 forks source link

[BUG] Optional[BackLinks] return beanie.odm.fields.BackLink object #749

Closed Nicialy closed 9 months ago

Nicialy commented 1 year ago

Describe the bug A clear and concise description of what the bug is. BackLink with None Create return dont None

To Reproduce


class Sprint(Document, SprintCreation):
    id: UUID = Field(default_factory=uuid4)
    issues: List[Link["Issue"]] = Field(default_factory=list)

class Issue(Document, IssueBase):
    id: UUID = Field(default_factory=uuid4)
    creation_date: datetime = Field(default_factory=datetime.now)
    sprint: Optional[BackLink["Sprint"]] = Field(default=None,original_field="issues", exclude=True)

issue = Issue(**issue_creation.model_dump(),sprint=None)
issue = await Issue.find_one(Issue.id == issue_id, fetch_links=True)
if issue.sprint == None or issue_creation.sprint_id != issue.sprint.id:
            issue.sprint.issues.remove(issue.id)

Expected behavior I want issue.sprint == None true

image image

roman-right commented 11 months ago

Hi! Could you please provide on-module example of this behavior which I can run locally to check, what is happening?

Nasochec commented 11 months ago

Example: main.py.txt

Nicialy commented 11 months ago

@roman-right

from motor.motor_asyncio import AsyncIOMotorClient

from typing import Optional
from pydantic import Field
import asyncio
from beanie import init_beanie,Document,BackLink,Link
from beanie.odm.fields import PydanticObjectId

class Country(Document):
    id: PydanticObjectId = Field(default_factory=PydanticObjectId)
    cities: list[Link["City"]] = Field(default_factory=list)

class City(Document):
    id: PydanticObjectId = Field(default_factory=PydanticObjectId)
    country: Optional[BackLink["Country"]] = Field(default=None, original_field="cities", exclude=True)

__beanie_models__=[Country,City]

async def fillSomeData(): 
    session:AsyncIOMotorClient  = AsyncIOMotorClient("mongodb://localhost:27017")
    await init_beanie(database=session.example, document_models=__beanie_models__)

    cityOk = City()
    await cityOk.create()
    # cityOk.country = <beanie.odm.fields.BackLink object at > expected None

    country = Country()
    country.cities.append(cityOk)
    await country.save()

    cityErr = City()
    await cityErr.create()
    # cityErr.country = <beanie.odm.fields.BackLink object at > expected None

    id = cityOk.id
    id1 = cityErr.id
    city = await City.find_one(City.id==id, fetch_links=True)
    city1 = await City.find_one(City.id==id1, fetch_links=True)

    print(city.country)#  country = Country(id=ObjectId('6548dd0f4 ... 
    print(city1.country)# country = <beanie.odm.fields.BackLink object at > expected None

if __name__ == "__main__":
    asyncio.run(fillSomeData())
Nicialy commented 11 months ago

they forgot about us...

roman-right commented 10 months ago

Hi @Nicialy and @Nasochec , Sorry for the delay.

I investigated this problem. It is a bit of trade off. BackLink is a virtual field. It means Beanie never stores this field to the database, it only populates it on finds with fetch_links=True. It means that if fetch_links != True Beanie has no information about if such relations exist or not. But Links (and BackLinks) can be fetched directly using fetch method. But to be able to run this method this field should be an object of BackLink. And, as Beanie on the filed level doesn't know if there is no such relation or relation was not fetched, it always creates an empty BackLink object for such fields.

I understand, that it is confusing. But I didn't find a good solution to handle it equally for all the cases. Like I can understand, that nothing was returned back using fetch_links==True and put None there, but I can not understand, what should be set there, if fetch_links==False. And I can not handle these two scenarios differently, as it will not be consistent.

I'm open for suggestions. If you have any ideas about this case, please share. We can discuss it in real-time on the Discord server. Or here, for sure.

Thank you for the catch. This topic definitely needs more investigation.

github-actions[bot] commented 9 months ago

This issue is stale because it has been open 30 days with no activity.

github-actions[bot] commented 9 months ago

This issue was closed because it has been stalled for 14 days with no activity.