Skip to content

Commit 893bcf3

Browse files
committed
Add example code for sa_column onupdate timestamps
1 parent 75ce455 commit 893bcf3

File tree

4 files changed

+108
-0
lines changed

4 files changed

+108
-0
lines changed

docs/advanced/sa-column.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# SQLAlchemy Columns
2+
3+
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.
4+
5+
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.().
6+
7+
## Columns for Timestamps
8+
9+
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.
10+
11+
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:
12+
13+
```{.python .annotate hl_lines="8 12"}
14+
{!./docs_src/advanced/sa_column/tutorial001.py[ln:9-21]!}
15+
```
16+
17+
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.
18+
19+
As there is a value there now, then it will not be changed automatically in the future.
20+
21+
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.
22+
23+
!!! warning
24+
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.
25+
26+
<details>
27+
<summary>👀 Full file preview</summary>
28+
29+
```Python
30+
{!./docs_src/advanced/sa_column/tutorial001.py!}
31+
```
32+
33+
</details>

docs_src/advanced/sa_column/__init__.py

Whitespace-only changes.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from datetime import datetime
2+
from time import sleep
3+
from typing import Optional
4+
5+
from sqlmodel import Field, Session, SQLModel, create_engine, select
6+
from sqlalchemy import Column, DateTime, func
7+
8+
9+
class Hero(SQLModel, table=True):
10+
id: Optional[int] = Field(default=None, primary_key=True)
11+
name: str
12+
secret_name: str
13+
age: Optional[int] = None
14+
15+
registered_at: datetime = Field(
16+
sa_column=Column(DateTime(timezone=False), server_default=func.now())
17+
)
18+
19+
updated_at: Optional[datetime] = Field(
20+
sa_column=Column(DateTime(timezone=False), onupdate=func.now())
21+
)
22+
23+
24+
sqlite_file_name = "database.db"
25+
sqlite_url = f"sqlite:///{sqlite_file_name}"
26+
27+
engine = create_engine(sqlite_url, echo=True)
28+
29+
30+
def create_db_and_tables():
31+
SQLModel.metadata.create_all(engine)
32+
33+
34+
def create_heroes():
35+
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
36+
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
37+
hero_3 = Hero(name="Rusty-Man", secret_name="Tommy Sharp", age=48)
38+
39+
session = Session(engine)
40+
41+
session.add(hero_1)
42+
session.add(hero_2)
43+
session.add(hero_3)
44+
45+
session.commit()
46+
47+
session.close()
48+
49+
50+
def update_hero_age(new_secret_name):
51+
with Session(engine) as session:
52+
statement = select(Hero).where(Hero.name == "Spider-Boy")
53+
results = session.exec(statement)
54+
hero = results.one()
55+
print("Hero:", hero)
56+
57+
hero.secret_name = new_secret_name
58+
session.add(hero)
59+
session.commit()
60+
session.refresh(hero)
61+
print("Updated hero:", hero)
62+
63+
64+
def main():
65+
create_db_and_tables()
66+
create_heroes()
67+
sleep(1)
68+
update_hero_age("Arachnid-Lad")
69+
sleep(1)
70+
update_hero_age("The Wallclimber")
71+
72+
73+
if __name__ == "__main__":
74+
main()

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ nav:
8585
- Advanced User Guide:
8686
- advanced/index.md
8787
- advanced/decimal.md
88+
- advanced/sa-column.md
8889
- alternatives.md
8990
- help.md
9091
- contributing.md

0 commit comments

Comments
 (0)