为什么 sqlalchemy 没有正确设置默认值?

Why is sqlalchemy not setting default value correctly?

出于某种原因,在此示例中,可选的 is_active 属性未设置为默认值。

from pydantic import BaseModel, EmailStr
from datetime import datetime

# Pydantic schemas

# Shared properties
class UserBase(BaseModel):
    email: Optional[EmailStr] = None
    is_active: Optional[bool] = True
    is_superuser: bool = False
    username: Optional[str] = None
    

# Properties to receive via API on creation
class UserCreate(UserBase):
    email: EmailStr
    password: str


# sqlalchemy model

class User(Base):
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String(25), index=True, unique=True, nullable=False)
    email = Column(String(50), unique=True, index=True, nullable=False)
    hashed_password = Column(String(256), nullable=False)
    is_active = Column(Boolean(), default=True, nullable=False)
    is_superuser = Column(Boolean(), default=False, nullable=False)

    __mapper_args__ = {"eager_defaults": True}


我期望作为可选输入的 is_active 的默认值为 True。但如果没有明确通过,我会得到 None

obj_in = UserCreate(email=email, password=password, username=username)
print(obj_in.is_active)
# True


db_obj = User(
            email=obj_in.email,
            hashed_password=get_password_hash(obj_in.password),
            username=obj_in.username,
            is_superuser=obj_in.is_superuser,
            # is_active=obj_in.is_active, 
        )
print(db_obj.is_active)
# None


# I have to add the is_active flag explicitly
db_obj = User(
            email=obj_in.email,
            hashed_password=get_password_hash(obj_in.password),
            username=obj_in.username,
            is_superuser=obj_in.is_superuser,
            is_active=obj_in.is_active, 
        )
print(db_obj.is_active)
# True
    

事后看来,非常明显。 (不是一切)。有两个问题:

  1. 我在实际插入数据库之前检查了 db_obj 的值。无论出于何种原因,我虽然 sqlalchemy Model db_obj = User(...) 返回的对象也会分配默认值。原来只有在将对象插入数据库后才会分配默认值。

  2. 影响我的用例的第二个问题是我没有刷新会话并在退出 with session.begin() 块之前尝试访问 obj id,即在提交对 db 的更改之前。

所以,修正这两点,我们得到:


from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.orm import sessionmaker

engine = create_async_engine(
        SQLALCHEMY_DATABASE_URI,
        echo=True,
    )
    
# expire_on_commit=False will prevent attributes from being expired
# after commit.
async_session = sessionmaker(
    engine, expire_on_commit=False, class_=AsyncSession
)


obj_in = UserCreate(email=email, password=password, username=username)

db_obj = User(
            email=obj_in.email,
            hashed_password=get_password_hash(obj_in.password),
            username=obj_in.username,
            is_superuser=obj_in.is_superuser,
            # is_active=obj_in.is_active, 
        )


# this was my use case for running tests.
# I start session.begin() in a pytest fixture, so I don't have
# control on exiting the with block to allow db commit. 
# So in this case, a force `flush` is required.
async with async_session() as session, session.begin():
    session.add(db_obj)

    # force commit 
    await session.flush()

    # this assertion fails if above flush is removed.
    assert db_obj.is_active == obj_in.is_active