-
-
Notifications
You must be signed in to change notification settings - Fork 845
📝 Add code example for sa_column onupdate timestamps
#372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
893bcf3
1e5caaa
01ca385
d78fc71
b160fc8
06e0471
bf882be
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| # SQLAlchemy Columns | ||
|
|
||
| In some cases you may need more control over the columns generated by SQLModel, this can be done by using the `sa_column`, `sa_column_args`, and `sa_column_kwargs` arguments when creating the `Field` object. | ||
|
|
||
| There are many use cases for this, but ones where this is particularity useful is when you want more advanced defaults for values than what is easy to implement with Pydantic, such `created_at` or `update_at` timestamps for rows.(). | ||
|
|
||
| ## Columns for Timestamps | ||
|
|
||
| Two ways of implementing `created_at` timestamps with Pydantic are [default factories](https://pydantic-docs.helpmanual.io/usage/models/#field-with-dynamic-default-value) and [validators](https://pydantic-docs.helpmanual.io/usage/validators/#validate-always), however there's no straightforward way to have an `update_at` timestamp. | ||
|
RobertRosca marked this conversation as resolved.
|
||
|
|
||
| The SQLAlchemy docs describe how `created_at` timestamps can be automatically set with either [default](https://docs.sqlalchemy.org/en/14/core/defaults.html#python-executed-functions) or [server-default](https://docs.sqlalchemy.org/en/14/core/defaults.html#server-invoked-ddl-explicit-default-expressions) functions, by using `sa_column=Column(...)` as described in the SQLAlchemy documentation we can achieve the same behaviour: | ||
|
|
||
| ```{.python .annotate hl_lines="8 12"} | ||
| {!./docs_src/advanced/sa_column/tutorial001.py[ln:9-21]!} | ||
| ``` | ||
|
YuriiMotov marked this conversation as resolved.
Outdated
|
||
|
|
||
| Above we are saying that the `registered_at` column should have a `server_default` value of `func.now()` (see full code for imports), which means that if there is no provided value then the current time will be the recorded value for that row. | ||
|
|
||
| As there is a value there now, then it will not be changed automatically in the future. | ||
|
|
||
| The `updated_at` column has an `onupdate` value of `func.now()`, this means that each time an `UPDATE` is performed, the function will be executed, meaning that the timestamp changes whenever a change is made to the row. | ||
|
|
||
| !!! warning | ||
| The difference between client-side python functions, server-side ddl expressions, and server-side implicit defaults is important in some situations but too in-depth to go into here. Check the SQL and SQLAlchemy docs for more information. | ||
|
YuriiMotov marked this conversation as resolved.
Outdated
|
||
|
|
||
| <details> | ||
| <summary>👀 Full file preview</summary> | ||
|
|
||
| ```Python | ||
| {!./docs_src/advanced/sa_column/tutorial001.py!} | ||
| ``` | ||
|
|
||
| </details> | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,74 @@ | ||||||
| from datetime import datetime | ||||||
| from time import sleep | ||||||
| from typing import Optional | ||||||
|
|
||||||
| from sqlalchemy import Column, DateTime, func | ||||||
| from sqlmodel import Field, Session, SQLModel, create_engine, select | ||||||
|
|
||||||
|
|
||||||
| class Hero(SQLModel, table=True): | ||||||
| id: Optional[int] = Field(default=None, primary_key=True) | ||||||
| name: str | ||||||
| secret_name: str | ||||||
| age: Optional[int] = None | ||||||
|
|
||||||
| registered_at: datetime = Field( | ||||||
| sa_column=Column(DateTime(timezone=False), server_default=func.now()) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||||||
| ) | ||||||
|
|
||||||
| updated_at: Optional[datetime] = Field( | ||||||
| sa_column=Column(DateTime(timezone=False), onupdate=func.now()) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add |
||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| sqlite_file_name = "database.db" | ||||||
| sqlite_url = f"sqlite:///{sqlite_file_name}" | ||||||
|
|
||||||
| engine = create_engine(sqlite_url, echo=True) | ||||||
|
|
||||||
|
|
||||||
| def create_db_and_tables(): | ||||||
| SQLModel.metadata.create_all(engine) | ||||||
|
|
||||||
|
|
||||||
| def create_heroes(): | ||||||
| hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson") | ||||||
| hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador") | ||||||
| hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48) | ||||||
|
Comment on lines
+36
to
+37
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We only update hero1, I would remove hero2 and hero3 to simplify the example |
||||||
|
|
||||||
| session = Session(engine) | ||||||
|
|
||||||
| session.add(hero_1) | ||||||
| session.add(hero_2) | ||||||
| session.add(hero_3) | ||||||
|
|
||||||
| session.commit() | ||||||
|
|
||||||
| session.close() | ||||||
|
|
||||||
|
|
||||||
| def update_hero_age(new_secret_name): | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| with Session(engine) as session: | ||||||
| statement = select(Hero).where(Hero.name == "Spider-Boy") | ||||||
| results = session.exec(statement) | ||||||
| hero = results.one() | ||||||
| print("Hero:", hero) | ||||||
|
|
||||||
| hero.secret_name = new_secret_name | ||||||
| session.add(hero) | ||||||
| session.commit() | ||||||
| session.refresh(hero) | ||||||
| print("Updated hero:", hero) | ||||||
|
|
||||||
|
|
||||||
| def main(): | ||||||
| create_db_and_tables() | ||||||
| create_heroes() | ||||||
| sleep(1) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to find way to avoid |
||||||
| update_hero_age("Arachnid-Lad") | ||||||
| sleep(1) | ||||||
| update_hero_age("The Wallclimber") | ||||||
|
|
||||||
|
|
||||||
| if __name__ == "__main__": | ||||||
| main() | ||||||
Uh oh!
There was an error while loading. Please reload this page.