MasoniteFramework / orm

Masonite ORM is a beautiful Python ORM. It's also a nearly drop in replacement of the Orator ORM
https://orm.masoniteproject.com
MIT License
160 stars 47 forks source link

masoniteorm.query.QueryBuilder.QueryBuilder.create() #843

Closed moluwole closed 9 months ago

moluwole commented 1 year ago

Hello guys, I have this blogpost https://testdriven.io/blog/masonite-orm-fastapi/.

I tried upgrading the dependencies of the post so as to have the latest versions but when I try to save to the database, I get this error

"masoniteorm.query.QueryBuilder.QueryBuilder.create() got multiple values for keyword argument 'id_key'"

The whole project works fine with the 2.18.6 version but anything higher than that results in the error above

josephmancuso commented 1 year ago

Can you put the whole code snippet here for me please? and also possibly any stack traces

moluwole commented 1 year ago

StackTrace

Traceback (most recent call last):
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/fastapi/applications.py", line 270, in __call__
    await super().__call__(scope, receive, send)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/routing.py", line 706, in __call__
    await route.handle(scope, receive, send)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/routing.py", line 66, in app
    response = await func(request)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/fastapi/routing.py", line 237, in app
    raw_response = await run_endpoint_function(
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/fastapi/routing.py", line 165, in run_endpoint_function
    return await run_in_threadpool(dependant.call, **values)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/starlette/concurrency.py", line 41, in run_in_threadpool
    return await anyio.to_thread.run_sync(func, *args)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/anyio/to_thread.py", line 31, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 937, in run_sync_in_worker_thread
    return await future
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 867, in run
    result = context.run(func, *args)
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/./main.py", line 36, in add_user
    user.save() # saves user details to the database.
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/masoniteorm/models/Model.py", line 870, in save
    result = self.create(
  File "/home/moluwole/Documents/code/writing/fastapi-masonite/.env/lib/python3.10/site-packages/masoniteorm/models/Model.py", line 565, in create
    return cls.builder.create(
TypeError: masoniteorm.query.QueryBuilder.QueryBuilder.create() got multiple values for keyword argument 'id_key'
moluwole commented 1 year ago
@app.post("/api/v1/users", response_model=schema.UserResult)
def add_user(user_data: schema.UserCreate):
    user = User.where("email", user_data.email).get()
    if user:
        raise HTTPException(status_code=400, detail="User already exists")
    user = User()
    user.email = user_data.email
    user.name = user_data.name
    user.address = user_data.address
    user.sex = user_data.sex
    user.phone_number = user_data.phone_number

    user.save() # saves user details to the database.
    return user
moluwole commented 1 year ago

@josephmancuso do note that this error does not occur on the 2.18.6 version of masonite-orm

moluwole commented 1 year ago

My models/User.py file

 """ User Model """

from masoniteorm.models import Model
from masoniteorm.relationships import has_many

class User(Model):
    """User Model"""

    @has_many("id", "user_id")
    def posts(self):
        from .Post import Post

        return Post

    @has_many("id", "user_id")
    def comments(self):
        from .Comment import Comment

        return Comment
moluwole commented 1 year ago

My Migration

"""MigrationForUserTable Migration."""

from masoniteorm.migrations import Migration

class MigrationForUserTable(Migration):
    def up(self):
        """
        Run the migrations.
        """
        with self.schema.create("users") as table:
            table.increments("id")
            table.string("name")
            table.string("email").unique()
            table.text("address").nullable()
            table.string("phone_number", 11).nullable()
            table.enum("sex", ["male", "female"]).nullable()
            table.timestamps()

    def down(self):
        """
        Revert the migrations.
        """
        self.schema.drop("Users")
josephmancuso commented 1 year ago

Have you tried on 2.19.1?

moluwole commented 1 year ago

Yup...that's the version giving me this error

josephmancuso commented 1 year ago

Thanks that actually gives me an idea of where the issue is. I'll take a look as soon as i can

langsamer commented 1 year ago

I just ran into the same bug. in models/Model.py, line 869, self.create() is called with keyword argument id_key=self.get_primary_key(), however the create method does not expect id_key as an argument, so it gets passed inside **kwargs. Within the method create() (line 565) cls.builder.create() is called with id_key=cls.__primary_key__ and **kwargs, so id_key now is passed twice.

tpow commented 1 year ago

Yes, I hit this as well. In my case I have a classmethod in my model that is essentially working like an alternative constructor. It does something like this:

from masoniteorm.models import Model

class MyClass(Model): 
    @classmethod                                                                 
    def create(cls, variable):
        c = cls()
        c.field = variable
        c.otherfield = 'something else'
        c.save()  # <-- fails with TypeError multiple values for id_key
        return c.id

The bug is caused by the Model.save adding the id_key when it calls create. This means Model.create gets the id_key in the kwargs, but then adds it again. Arguably, save doesn't need to add it.

I worked around this by overriding the create function in my model. I simply lifted the create from Model and then changed it like below.

    @classmethod                                                                 
    def create(                                                                  
        cls,                                                                     
        dictionary: Dict[str, Any] = None,                                       
        query: bool = False,                                                     
        cast: bool = False,                                                      
        **kwargs,                                                                
    ):                                                                           
        """Creates new records based off of a dictionary as well as data set on the model
        such as fillable values.                                                 

        Args:                                                                    
            dictionary (dict, optional): [description]. Defaults to {}.          
            query (bool, optional): [description]. Defaults to False.            
            cast (bool, optional): [description]. Whether or not to cast passed values.

        Returns:                                                                 
            self: A hydrated version of a model                                  
        """                                                                      
        if query:                                                                
            return cls.builder.create(                                           
                dictionary, query=True, id_key=cls.__primary_key__, cast=cast, **kwargs
            ).to_sql()                                                           

        return cls.builder.create(                                               
            # dictionary, id_key=cls.__primary_key__, cast=cast, **kwargs  # <-- was this
            dictionary, cast=cast, **kwargs  # <-- It should be this       
        )  

Again, looking at the code history, it looks the real problem is that save is adding the id_key, but changing create like above would also fix it.

@josephmancuso It'd be nice if you could take a look at this and get a fix released. Thanks! Tim

josephmancuso commented 9 months ago

this is fixed in v2.19.2