BeanieODM / beanie

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

[BUG] All documents get dumped into "Documents" collection since 1.14.0 #422

Closed Luc1412 closed 1 year ago

Luc1412 commented 1 year ago

Describe the bug Since 1.14.0 all my documents get inserted into one collection named "Documents". All documents also got a _class_id value with .UserEntry

To Reproduce

class UserEntry(Document, ABC):
    id: int
    value: Optional[str] = None
    ...

    class Settings:
        name = 'user_data'
        use_state_management = True

user_data = await UserEntry(...)
user_data.value = 'Test'
await user_data.insert()

Expected behavior The document get's inserted into a "user_data" collection.

Additional context https://github.com/roman-right/beanie/compare/1.13.1...1.14.0

roman-right commented 1 year ago

I tried this against 1.13.1, 1.14.0, and the current 1.15.3. It inserts into the collection user_data

Full script:

import asyncio
from abc import ABC
from typing import Optional

from motor.motor_asyncio import AsyncIOMotorClient

from beanie import Document, init_beanie

class UserEntry(Document, ABC):
    id: int
    value: Optional[str] = None

    class Settings:
        name = 'user_data'
        use_state_management = True

async def main():
    cli = AsyncIOMotorClient("mongodb://test:test@localhost:27017")
    db = cli["test_find_one"]
    await init_beanie(db, document_models=[UserEntry])

    await UserEntry(id=2, value="Test").insert()

asyncio.run(main())

Please, correct my code to reproduce this error.

Juggerr commented 1 year ago

@roman-right Hi, I've got this issue as well. Before ~1.13 I used the following pattern: Have and extended base document class:

class BetterDocument(Document):
    @classmethod
    async def get_or_create(cls, data):
        record = await cls.find_one(data)
        if not record:
            record = cls(**data)
            await record.insert()
        return record

    @classmethod
    async def get_or_update(cls, lookup, data):
        record = await cls.find_one(lookup)
        all_data = {**lookup, **data}
        if not record:
            record = cls(**all_data)
            await record.insert()
        else:
            await record.update(all_data)
        return record

    @classmethod
    async def get_edged(cls, lookup=None, default=None, direction=DESCENDING, sort_by="created"):
        umongo_cursor = cls.find(lookup if lookup else {}).sort([(sort_by, direction)]).limit(1)
        async for record in umongo_cursor:
            return record
        return default

    @classmethod
    async def get_latest(cls, lookup=None, default=None):
        return await cls.get_edged(lookup, default, direction=DESCENDING)

    @classmethod
    async def get_earliest(cls, lookup=None, default=None):
        return await cls.get_edged(lookup, default, direction=ASCENDING)

Then I inherited it for all my models in a project:

...

class PlayerIdentity(BaseModel):
    external_id: Indexed(str, unique=True)
    username: Indexed(str, unique=True)
    demo_player: bool = False

class PlayerStatistics(BaseModel):
    total_bets: int = 0
    total_wins: int = 0

class Players(PlayerIdentity, PlayerStatistics, BetterDocument):
    created: datetime = Field(default_factory=datetime.utcnow)
    integration: Link[Integrations]
    game_settings: PlayerGameSettings = Field(default_factory=PlayerGameSettings)

...

Everything worked perfectly until (I guess... didn't notice exactly) - v13.2. After that version, I've got created a document called BetterDocument Note: I didn't put this document to the document_models list into the init_beanie call Fields of that document are all fields from all documents inherited from it + field called _class_id with the name of the childer document with . prefix, e.g.:

Screenshot 2022-11-18 at 09 06 35

Thank you in advance! love beanie very much!

Luc1412 commented 1 year ago

Juggerr thanks for sharing your case. I simplified my example strongly, so this might be the reason for not causing the issue.

I also subclassed the Document class (overwrite save_changes ti insert the doc if not exists) and called it Document. Seems like it's connected with that. I'll investigate that later.

roman-right commented 1 year ago

Thank you for your input @Juggerr ,

Could you guys please make a script that I can run to see the problem? This bug looks pretty important but I can not reproduce it.

I use your snippets - put them into my dummy inserter and don't see this bug:

import asyncio
from datetime import datetime

from beanie import Document, init_beanie
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel, Field

class BetterDocument(Document):
    ...

class PlayerIdentity(BaseModel):
    one: str = "one"

class PlayerStatistics(BaseModel):
    total_bets: int = 0
    total_wins: int = 0

class Players(PlayerIdentity, PlayerStatistics, BetterDocument):
    created: datetime = Field(default_factory=datetime.utcnow)

async def main():
    cli = AsyncIOMotorClient("mongodb://beanie:beanie@localhost:27017")
    db = cli["test_wrong_collection"]
    await init_beanie(db, document_models=[Players])

    await Players().insert()

asyncio.run(main())

Beanie versions: 1.13.1, 1.14.0, 1.15.x

Vitalium commented 1 year ago

Here is my example:

import asyncio
from pydantic import BaseModel, Field
from motor.motor_asyncio import AsyncIOMotorClient
from beanie import Document, init_beanie

class Mixin(BaseModel):
    id: int = Field(..., ge=1, le=254)

class MyDoc(Document):
    class Settings:
        use_state_management = True

class Test(Mixin, MyDoc):
    name: str

class Test2(MyDoc):
    name: str

async def main():
    client = AsyncIOMotorClient()
    await init_beanie(database=client.b, document_models=[Test, Test2])
    t1 = await Test(id=1, name='test').create()
    print('Created', t1)
    t2 = await Test2(name='test2').create()
    print('Created', t2)
    await t1.delete()
    await t2.delete()

if __name__ == '__main__':
    asyncio.run(main())

Beanie version 1.15.3. Using mongosh I see only "MyDoc" and "Test" collections.

Here is data inside MyDoc collection (before t2 is deleted):

b> db.MyDoc.find()
[
  {
    _id: ObjectId("637761b89fe3d9d138f0fbf8"),
    _class_id: '.Test2',
    name: 'test2'
  }
]

This issue is related to the inheritance feature.

roman-right commented 1 year ago

Thank you, @Vitalium ! Reproduced. This will be fixed today

roman-right commented 1 year ago

@Luc1412 , @Juggerr , @Vitalium please try 1.15.4

Luc1412 commented 1 year ago

works fine. Thanks for fixing it.

Juggerr commented 1 year ago

@roman-right It works, thank you very much!