Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/advanced/sa-column.md
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.().
Comment thread
RobertRosca marked this conversation as resolved.
Outdated

## 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.
Comment thread
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]!}
```
Comment thread
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.
Comment thread
YuriiMotov marked this conversation as resolved.
Outdated

<details>
<summary>👀 Full file preview</summary>

```Python
{!./docs_src/advanced/sa_column/tutorial001.py!}
```

</details>
Empty file.
74 changes: 74 additions & 0 deletions docs_src/advanced/sa_column/tutorial001.py
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())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add nullable=False?

)

updated_at: Optional[datetime] = Field(
sa_column=Column(DateTime(timezone=False), onupdate=func.now())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add server_default=func.now() and nullable=False?

)


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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def update_hero_age(new_secret_name):
def update_hero_secret_name(new_secret_name):

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to find way to avoid sleeping as it will make test run slower

update_hero_age("Arachnid-Lad")
sleep(1)
update_hero_age("The Wallclimber")


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ nav:
- Advanced User Guide:
- advanced/index.md
- advanced/decimal.md
- advanced/sa-column.md
- alternatives.md
- help.md
- contributing.md
Expand Down