Search code examples
sqlmodel

How can I create two fields in a SQLModel model with identical default datetimes?


I have the following model definitions in SQLModel:

class UserBase(SQLModel):
    first_name: str = Field(min_length=2, max_length=50)
    last_name: str = Field(min_length=2, max_length=50)
    email: EmailStr


class User(UserBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    created_at: datetime = Field(default_factory=get_current_utc_time)
    updated_at: datetime = Field(default_factory=get_current_utc_time)
    last_login_at: datetime | None = None
    email: EmailStr = Field(unique=True)

where get_current_utc_time is a function that simply returns datetime.now(UTC). I would like created_at and updated_at to be exactly the same when a user is first created in the database. This currently doesn't happen because get_current_utc_time is called twice.

I tried the following alteration:

updated_at: datetime = created_at

Bizarrely, the value of updated_at is still different from created_at.

Could anyone help with the following questions:

  • Why is the value of updated_at still different from created_at?
  • Is it possible to achieve the desired behaviour "out of the box" with SQLModel?
  • Could I write a custom validator to achieve this, and if so how would I ensure that it is called by SQLModel, even though table=True? (given this behaviour: https://github.com/fastapi/sqlmodel/issues/52)

Solution

  • When you did update_at: datetime = created_at it did not work, because python judges created_at at class definition time not instance creation time. They were different because basically default values are evaluated independently at instance creation time.

    Solution to this could be during model creation setting updated_at to None and then during initializing setting it as created_at...

    class User(UserBase, table=True):
        id: int | None = Field(default=None, primary_key=True)
        created_at: datetime = Field(default_factory=get_current_utc_time)
        updated_at: datetime | None = None
        last_login_at: datetime | None = None
        email: EmailStr = Field(unique=True)
    
        def __init__(self, **data):
            super().__init__(**data)
            if self.updated_at is None:
                 self.updated_at = self.created_at
    

    The solution will solve the problem but the more maintainable solution could be create a paired factory function, something like...

    inital_time_factory, shared_time_factory = create_timestamp_pair()
    
    
    class User(UserBase, table=True):
        id: int | None = Field(default=None, primary_key=True)
        created_at: datetime = Field(default_factory=initial_time_factory)
        updated_at: datetime = Field(default_factory=shared_time_factory)
        last_login_at: datetime | None = None
        email: EmailStr = Field(unique=True)
    

    the create_timestamp_pair() creates a pair of factory functions that returns the same timestamp.

    Finally, I dont think validators to be the best solution, because iguess they are executed after the fields are set, not sure about this, you will have to manage validation order properly.