We will continue from where we left of in the last chapter.
This is the code we had to create the database and table, nothing new here:
fromsqlmodelimportField,SQLModel,create_engineclassHero(SQLModel,table=True):id:int|None=Field(default=None,primary_key=True)name:strsecret_name:strage:int|None=Nonesqlite_file_name="database.db"sqlite_url=f"sqlite:///{sqlite_file_name}"engine=create_engine(sqlite_url,echo=True)defcreate_db_and_tables():SQLModel.metadata.create_all(engine)# More code here later πif__name__=="__main__":create_db_and_tables()
Import Optional from typing to declare fields that could be None.
fromtypingimportOptionalfromsqlmodelimportField,SQLModel,create_engineclassHero(SQLModel,table=True):id:Optional[int]=Field(default=None,primary_key=True)name:strsecret_name:strage:Optional[int]=Nonesqlite_file_name="database.db"sqlite_url=f"sqlite:///{sqlite_file_name}"engine=create_engine(sqlite_url,echo=True)defcreate_db_and_tables():SQLModel.metadata.create_all(engine)# More code here later πif__name__=="__main__":create_db_and_tables()
Now that we can create the database and the table, we will continue from this point and add more code on the same file to create the data.
When working with a database (SQL or any other type) in a programming language, we will always have some data in memory, in objects and variables we create in our code, and there will be some data in the database.
We are constantly gettingsome of the data from the database and putting it in memory, in variables.
The same way, we are constantly creating variables and objects with data in our code, that we then want to save in the database, so we send it somehow.
In some cases, we can even create some data in memory and then change it and update it before saving it in the database.
We might even decide with some logic in the code that we no longer want to save the data in the database, and then just remove it. π₯ And we only handled that data in memory, without sending it back and forth to the database.
SQLModel does all it can (actually via SQLAlchemy) to make this interaction as simple, intuitive, and familiar or "close to programming" as possible. β¨
But that division of the two places where some data might be at each moment in time (in memory or in the database) is always there. And it's important for you to have it in mind. π€
First, remove that file database.db so we can start from a clean slate.
Because we have Python code executing with data in memory, and the database is an independent system (an external SQLite file, or an external database server), we need to perform two steps:
create the data in Python, in memory (in a variable)
Let's start with the first step, create the data in memory.
We already created a class Hero that represents the hero table in the database.
Each instance we create will represent the data in a row in the database.
So, the first step is to simply create an instance of Hero.
We'll create 3 right away, for the 3 heroes:
# Code above omitted πdefcreate_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)# More code here later π
# Code above omitted πdefcreate_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)# More code here later π
Up to now, we have only used the engine to interact with the database.
The engine is that single object that we share with all the code, and that is in charge of communicating with the database, handling the connections (when using a server database like PostgreSQL or MySQL), etc.
But when working with SQLModel you will mostly use another tool that sits on top, the Session.
In contrast to the engine that is one for the whole application, we create a new session for each group of operations with the database that belong together.
In fact, the session needs and uses an engine.
For example, if we have a web application, we would normally have a single session per request.
We would re-use the same engine in all the code, everywhere in the application (shared by all the requests). But for each request, we would create and use a new session. And once the request is done, we would close the session.
# Code above omitted πdefcreate_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)session=Session(engine)# More code here later π
# Code above omitted πdefcreate_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)session=Session(engine)# More code here later π
Now that we have some hero model instances (some objects in memory) and a session, the next step is to add them to the session:
# Code above omitted πdefcreate_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)session=Session(engine)session.add(hero_1)session.add(hero_2)session.add(hero_3)# More code here later π
# Code above omitted πdefcreate_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)session=Session(engine)session.add(hero_1)session.add(hero_2)session.add(hero_3)# More code here later π
By this point, our heroes are not stored in the database yet.
And this is one of the cases where having a session independent of an engine makes sense.
The session is holding in memory all the objects that should be saved in the database later.
And once we are ready, we can commit those changes, and then the session will use the engine underneath to save all the data by sending the appropriate SQL to the database, and that way it will create all the rows. All in a single batch.
This makes the interactions with the database more efficient (plus some extra benefits).
Technical Details
The session will create a new transaction and execute all the SQL code in that transaction.
This ensures that the data is saved in a single batch, and that it will all succeed or all fail, but it won't leave the database in a broken state.
Now that we have the heroes in the session and that we are ready to save all that to the database, we can commit the changes:
# Code above omitted πdefcreate_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)session=Session(engine)session.add(hero_1)session.add(hero_2)session.add(hero_3)session.commit()# More code here later π
# Code above omitted πdefcreate_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)session=Session(engine)session.add(hero_1)session.add(hero_2)session.add(hero_3)session.commit()# More code here later π
But to keep things a bit more organized, let's instead create a new function main() that will contain all the code that should be executed when called as an independent script, and we can put there the previous function create_db_and_tables(), and add the new function create_heroes():
# Code above omitted πdefmain():create_db_and_tables()create_heroes()# More code here later π
# Code above omitted πdefmain():create_db_and_tables()create_heroes()# More code here later π
Now we can run our program as a script from the console.
Because we created the engine with echo=True, it will print out all the SQL code that it is executing:
fast βpython app.pyπ¬ Some boilerplate, checking that the hero table already existsINFO Engine BEGIN (implicit) INFO Engine PRAGMA main.table_info("hero") INFO Engine [raw sql] () INFO Engine COMMITπ¬ BEGIN a transaction automatically β¨INFO Engine BEGIN (implicit)π¬ Our INSERT statement, it uses VALUES (?, ?, ?) as parametersINFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?)π¬ ...and these are the parameter values πINFO Engine [generated in 0.00013s] ('Deadpond', 'Dive Wilson', None)π¬ Again, for Spider-BoyINFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) INFO Engine [cached since 0.000755s ago] ('Spider-Boy', 'Pedro Parqueador', None)π¬ And now for Rusty-ManINFO Engine INSERT INTO hero (name, secret_name, age) VALUES (?, ?, ?) INFO Engine [cached since 0.001014s ago] ('Rusty-Man', 'Tommy Sharp', 48)π¬ All good? Yes, commit this transaction! πINFO Engine COMMIT
The session holds some resources, like connections from the engine.
So once we are done with the session, we should close it to make it release those resources and finish its cleanup:
# Code above omitted πdefcreate_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)session=Session(engine)session.add(hero_1)session.add(hero_2)session.add(hero_3)session.commit()session.close()# More code here later π
# Code above omitted πdefcreate_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)session=Session(engine)session.add(hero_1)session.add(hero_2)session.add(hero_3)session.commit()session.close()# More code here later π
It's good to know how the Session works and how to create and close it manually. It might be useful if, for example, you want to explore the code in an interactive session (for example with Jupyter).
But there's a better way to handle the session, using a with block:
This is the same as creating the session manually and then manually closing it. But here, using a with block, it will be automatically created when starting the with block and assigned to the variable session, and it will be automatically closed after the with block is finished.
And it will work even if there's an exception in the code. π
Now you know how to add rows to the database. π
Now is a good time to understand better why the id field can't be NULL on the database because it's a primary key, but actually can be None in the Python code.
I'll tell you about that in the next chapter. π