I recently was working on a project that uses the great work of sqlmodel.
I ran into a funny issue though!
Take the following example:
from typing import List from sqlmodel import Field, Session, select, SQLModel, Column, JSON, create_engine from rich import print sqlite_file_name = "test.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url) SQLModel.metadata.create_all(engine) class Actor(SQLModel, table=True): name: str = Field(primary_key=True) movies: List[str] = Field(sa_column=Column(JSON)) def __init__(self, name: str, movies: List[str]): self.name = name self.movies = movies def create_db_and_tables(): SQLModel.metadata.create_all(engine) def create_actors(): """Creates two actors and commits them to the database""" actor_one = Actor( name="Bob Dylan", movies=["Pat Garrett and Billy the Kid", "Renaldo and Clara"] ) actor_two = Actor(name="Lady Gaga", movies=["A Star Is Born", "House of Gucci"]) session = Session(engine) session.add(actor_one) session.add(actor_two) session.commit() def update_actors(): with Session(engine) as session: # Get actor one statement = select(Actor).where(Actor.name == "Bob Dylan") results = session.exec(statement) actor_one = results.one() print("Actor one: ", actor_one.json()) actor_one.movies.append("The Godfather") print("--------- Movie Appended ---------") print("Actor one: ", actor_one.json()) session.add(actor_one) print() # Get actor two statement = select(Actor).where(Actor.name == "Lady Gaga") results = session.exec(statement) actor_two = results.one() print("Actor two: ", actor_two.json()) actor_two.movies.append("The Many Saints of Newark") print("--------- Movie Appended ---------") print("Actor two: ", actor_two.json()) session.add(actor_two) session.commit() session.refresh(actor_one) session.refresh(actor_two) print("-------------- Done! --------------") create_db_and_tables() create_actors() update_actors()
This script does a few things, let's break it down:
Sets up a test database:
sqlite_file_name = "test.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url) SQLModel.metadata.create_all(engine)
Creates the model for an
Actor
:class Actor(SQLModel, table=True): name: str = Field(primary_key=True) movies: List[str] = Field(sa_column=Column(JSON)) def __init__(self, name: str, movies: List[str]): self.name = name self.movies = movies
This is an SQLModel, which creates a table on the sqlite3 Database.
Actor.name
is required, and is the primary key of the database table.These are unique values.
Actor.movies
is an explicit List of strings.def __init__(self, name: str, movies: List[str]): self.name = name self.movies = movies
The
__init__
method of Actor
requires the same types as described above.We now create two Actors and commit them to the database by running the
create_actors()
function.def create_actors(): """Creates two actors and commits them to the database""" actor_one = Actor( name="Bob Dylan", movies=["Pat Garrett and Billy the Kid", "Renaldo and Clara"] ) actor_two = Actor(name="Lady Gaga", movies=["A Star Is Born", "House of Gucci"]) session = Session(engine) session.add(actor_one) session.add(actor_two) session.commit()
Now we define a function to Select and Update the actors from the Database.
def update_actors(): with Session(engine) as session: # Get actor one statement = select(Actor).where(Actor.name == "Bob Dylan") results = session.exec(statement) actor_one = results.one() print("Actor one: ", actor_one.json()) actor_one.movies.append("The Godfather") print("--------- Movie Appended ---------") print("Actor one: ", actor_one.json()) session.add(actor_one) ...
Here we're also going to print two things!
- The first actor, outputted as json
- The first actor, after appending the movie
"The Godfather"
to the list of movies.
Then we're going to add the
actor_one
Actor
object to the session.Next, we're going to do the same thing to
actor_two
: ... # Get actor two statement = select(Actor).where(Actor.name == "Lady Gaga") results = session.exec(statement) actor_two = results.one() print("Actor two: ", actor_two.json()) actor_two.movies.append("The Many Saints of Newark") print("--------- Movie Appended ---------") print("Actor two: ", actor_two.json()) session.add(actor_two) session.commit() session.refresh(actor_one) session.refresh(actor_two) print("-------------- Done! --------------")
So now let's run it!
Output:
gorm@gorm.dev python main.py Actor one: {"movies": ["Pat Garrett and Billy the Kid", "Renaldo and Clara"], "name": "Bob Dylan"} --------- Movie Appended --------- Actor one: {"movies": ["Pat Garrett and Billy the Kid", "Renaldo and Clara", "The Godfather"], "name": "Bob Dylan"} Actor two: {"movies": ["A Star Is Born", "House of Gucci"], "name": "Lady Gaga"} --------- Movie Appended --------- Actor two: {"movies": ["A Star Is Born", "House of Gucci", "The Many Saints of Newark"], "name": "Lady Gaga"} -------------- Done! --------------
Interesting!
It looks like we got our update!
Now let's fire up DB Browser for SQLite.
brew install --cask db-browser-for-sqlite
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F333c02f2-2a02-46f7-89fe-b0ce74b806c1%2FUntitled.png?table=block&id=34c906a1-cbcf-4ced-8e43-aab3aba514a9&cache=v2)
Huh.
Well that's odd.
It turns out, we didn't update those database entries.
Where did this happen exactly?
Let's get a little more verbose.
I'm going to remove the
print
statements in update_actors
and then turn on echo=True
on the engine.sqlite_file_name = "test.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url, echo=True) #<--------------- SQLModel.metadata.create_all(engine)
Now let's remove the database:
gorm@gorm.dev rm test.db
Run the program again:
gorm@gorm.dev python main.py 2021-11-10 21:07:07,554 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-11-10 21:07:07,554 INFO sqlalchemy.engine.Engine COMMIT 2021-11-10 21:07:07,556 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-11-10 21:07:07,556 INFO sqlalchemy.engine.Engine PRAGMA main.table_info("actor") 2021-11-10 21:07:07,557 INFO sqlalchemy.engine.Engine [raw sql] () 2021-11-10 21:07:07,557 INFO sqlalchemy.engine.Engine PRAGMA temp.table_info("actor") 2021-11-10 21:07:07,557 INFO sqlalchemy.engine.Engine [raw sql] () 2021-11-10 21:07:07,557 INFO sqlalchemy.engine.Engine CREATE TABLE actor ( movies JSON, name VARCHAR NOT NULL, PRIMARY KEY (name) ) 2021-11-10 21:07:07,557 INFO sqlalchemy.engine.Engine [no key 0.00006s] () 2021-11-10 21:07:07,559 INFO sqlalchemy.engine.Engine CREATE INDEX ix_actor_name ON actor (name) 2021-11-10 21:07:07,559 INFO sqlalchemy.engine.Engine [no key 0.00007s] () 2021-11-10 21:07:07,560 INFO sqlalchemy.engine.Engine COMMIT 2021-11-10 21:07:07,560 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-11-10 21:07:07,561 INFO sqlalchemy.engine.Engine INSERT INTO actor (movies, name) VALUES (?, ?) 2021-11-10 21:07:07,561 INFO sqlalchemy.engine.Engine [generated in 0.00013s] (('["Pat Garrett and Billy the Kid", "Renaldo and Clara"]', 'Bob Dylan'), ('["A Star Is Born", "House of Gucci"]', 'Lady Gaga')) 2021-11-10 21:07:07,562 INFO sqlalchemy.engine.Engine COMMIT 2021-11-10 21:07:07,563 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-11-10 21:07:07,563 INFO sqlalchemy.engine.Engine SELECT actor.movies, actor.name FROM actor WHERE actor.name = ? 2021-11-10 21:07:07,563 INFO sqlalchemy.engine.Engine [no key 0.00007s] ('Bob Dylan',) 2021-11-10 21:07:07,564 INFO sqlalchemy.engine.Engine SELECT actor.movies, actor.name FROM actor WHERE actor.name = ? 2021-11-10 21:07:07,564 INFO sqlalchemy.engine.Engine [no key 0.00008s] ('Lady Gaga',) 2021-11-10 21:07:07,565 INFO sqlalchemy.engine.Engine COMMIT 2021-11-10 21:07:07,565 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-11-10 21:07:07,566 INFO sqlalchemy.engine.Engine SELECT actor.movies, actor.name FROM actor WHERE actor.name = ? 2021-11-10 21:07:07,566 INFO sqlalchemy.engine.Engine [generated in 0.00008s] ('Bob Dylan',) 2021-11-10 21:07:07,567 INFO sqlalchemy.engine.Engine SELECT actor.movies, actor.name FROM actor WHERE actor.name = ? 2021-11-10 21:07:07,567 INFO sqlalchemy.engine.Engine [cached since 0.0006238s ago] ('Lady Gaga',) 2021-11-10 21:07:07,567 INFO sqlalchemy.engine.Engine ROLLBACK
Notice that there's no updates!
Just to make sure we're sane, let's open up an interactive shell:
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F0394a711-598a-4928-8aa4-d4763917de3f%2FUntitled.png?table=block&id=45fa7300-9de6-4b17-824e-d7abe9c2452f&cache=v2)
Okay...
Now let's add this to the database...
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F97ae9df2-b750-40c8-8afe-c6dcbed1732f%2FUntitled.png?table=block&id=cb27eb63-f43b-4ffc-90a3-7e6502de310a&cache=v2)
What.. is happening?
Let's break it down to the basics.
Quick python refresher.
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F3f4cea89-a3ef-4a14-906e-b16a88a23fb6%2FUntitled.png?table=block&id=80d60ad8-19ea-4251-b107-239d4739b785&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9779cd3f-32bd-4b9c-ac12-65c7148683f8%2FUntitled.png?table=block&id=b2516841-57de-47d1-a7de-26d7493dd6d5&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F60242206-8826-4816-8f6b-3ffa6b06fffc%2FUntitled.png?table=block&id=65e0dd6f-ad84-489c-8265-4256670652ad&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F7c95147e-9efd-486a-a561-5843249954fc%2FUntitled.png?table=block&id=da52c52d-2e23-4424-9ee9-bdc583ac2d62&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fcaf5462c-fdd5-4409-a30c-7fa0e3e9c392%2FUntitled.png?table=block&id=0cd64eef-d746-480e-b780-b83f11a5d82c&cache=v2)
Next, let's try with some lists...
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Ff5f68f89-0a8c-4a2b-928a-39a71e076b03%2FUntitled.png?table=block&id=6e636d90-097b-4e6d-bd74-a3ca9ae86d82&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fa8dc10e8-a9ca-47dd-b0c2-b0a27bb47d11%2FUntitled.png?table=block&id=b3665583-8ef0-454d-aa23-6d3c67fb16e7&cache=v2)
Ok, so what's going on here?
It seems that
actor_one.movies
hasn't... really changed?Let's try to switch up our code.
This didn't seem to work:
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F71c7cbfe-b03d-43e6-97e9-ad0452a39b1c%2FUntitled.png?table=block&id=ec414e85-3b55-42cf-9874-db0a6c744994&cache=v2)
What now?..
Ok, let's try making a new object.
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fbd99aa5b-6e04-4473-af8c-359f69acfb66%2FUntitled.png?table=block&id=2bef469c-64e5-4158-ac42-55676e4c210a&cache=v2)
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Fc9231024-9013-46b3-8a8c-2af82a0fd3ad%2FUntitled.png?table=block&id=061a7bd2-823f-40ba-9014-e24033dc277f&cache=v2)
Output:
2021-11-10 21:37:11,580 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-11-10 21:37:11,582 INFO sqlalchemy.engine.Engine UPDATE actor SET movies=? WHERE actor.name = ? 2021-11-10 21:37:11,582 INFO sqlalchemy.engine.Engine [generated in 0.00015s] ('["Pat Garrett and Billy the Kid", "Renaldo and Clara", "The Godfather"]', 'Bob Dylan') 2021-11-10 21:37:11,583 INFO sqlalchemy.engine.Engine COMMIT 2021-11-10 21:37:11,584 INFO sqlalchemy.engine.Engine BEGIN (implicit) 2021-11-10 21:37:11,585 INFO sqlalchemy.engine.Engine SELECT actor.movies, actor.name FROM actor WHERE actor.name = ? 2021-11-10 21:37:11,585 INFO sqlalchemy.engine.Engine [cached since 1597s ago] ('Bob Dylan',) 2021-11-10 21:37:11,585 INFO sqlalchemy.engine.Engine ROLLBACK
Notice these lines:
UPDATE actor SET movies=? WHERE actor.name = ? [generated in 0.00015s] ('["Pat Garrett and Billy the Kid", "Renaldo and Clara", "The Godfather"]', 'Bob Dylan')
✨ Nice! ✨
Now, taking what we've learned, let's update our program.
from typing import List from sqlmodel import Field, Session, select, SQLModel, Column, JSON, create_engine from rich import print sqlite_file_name = "test.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url) SQLModel.metadata.create_all(engine) class Actor(SQLModel, table=True): name: str = Field(primary_key=True) movies: List[str] = Field(sa_column=Column(JSON)) def __init__(self, name: str, movies: List[str]): self.name = name self.movies = movies def create_db_and_tables(): SQLModel.metadata.create_all(engine) def create_actors(): """Creates two actors and commits them to the database""" actor_one = Actor( name="Bob Dylan", movies=["Pat Garrett and Billy the Kid", "Renaldo and Clara"] ) actor_two = Actor(name="Lady Gaga", movies=["A Star Is Born", "House of Gucci"]) session = Session(engine) session.add(actor_one) session.add(actor_two) session.commit() def update_actors(): with Session(engine) as session: # Get actor one statement = select(Actor).where(Actor.name == "Bob Dylan") results = session.exec(statement) actor_one = results.one() print("Actor one: ", actor_one.json()) movies = [] movies += actor_one.movies movies += ["The Godfather"] actor_one.movies = movies print("--------- Movie Appended ---------") print("Actor one: ", actor_one.json()) session.add(actor_one) print() # Get actor two statement = select(Actor).where(Actor.name == "Lady Gaga") results = session.exec(statement) actor_two = results.one() print("Actor two: ", actor_two.json()) movies = [] movies += actor_two.movies movies += ["The Many Saints of Newark"] actor_two.movies = movies print("--------- Movie Appended ---------") print("Actor two: ", actor_two.json()) session.add(actor_two) session.commit() session.refresh(actor_one) session.refresh(actor_two) print("-------------- Done! --------------") create_db_and_tables() create_actors() update_actors()
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2Ffd8fe153-6271-4cb8-8087-6e7b33fdc787%2FUntitled.png?table=block&id=b5961953-e37e-412b-b09d-7065d221825e&cache=v2)
Output:
gorm@gorm.dev python main.py Actor one: {"movies": ["Pat Garrett and Billy the Kid", "Renaldo and Clara"], "name": "Bob Dylan"} --------- Movie Appended --------- Actor one: {"movies": ["Pat Garrett and Billy the Kid", "Renaldo and Clara", "The Godfather"], "name": "Bob Dylan"} Actor two: {"movies": ["A Star Is Born", "House of Gucci"], "name": "Lady Gaga"} --------- Movie Appended --------- Actor two: {"movies": ["A Star Is Born", "House of Gucci", "The Many Saints of Newark"], "name": "Lady Gaga"} -------------- Done! --------------
And just to be sure, let's open up the database!
![notion image](https://www.notion.so/image/https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F1b42ba58-b07b-4510-9ffc-8d4600b5aac1%2FUntitled.png?table=block&id=5f9346cd-5c46-444f-822c-e0a4205e0483&cache=v2)
Sure enough!
Hope you learned something new!
Maybe I'll even save you a few hours of debugging. 🥴
Related Readings: