UUID (Universally Unique Identifiers)¶
We have discussed some data types like str
, int
, etc.
There's another data type called UUID
(Universally Unique Identifier).
You might have seen UUIDs, for example in URLs. They look something like this:
4ff2dab7-bffe-414d-88a5-1826b9fea8df
UUIDs can be particularly useful as an alternative to auto-incrementing integers for primary keys.
Info
Official support for UUIDs was added in SQLModel version 0.0.20
.
About UUIDs¶
UUIDs are numbers with 128 bits, that is, 16 bytes.
They are normally seen as 32 hexadecimal characters separated by dashes.
There are several versions of UUID, some versions include the current time in the bytes, but UUIDs version 4 are mainly random, the way they are generated makes them virtually unique.
Distributed UUIDs¶
You could generate one UUID in one computer, and someone else could generate another UUID in another computer, and it would be almost impossible for both UUIDs to be the same.
This means that you don't have to wait for the DB to generate the ID for you, you can generate it in code before sending it to the database, because you can be quite certain it will be unique.
Technical Details
Because the number of possible UUIDs is so large (2^128), the probability of generating the same UUID version 4 (the random ones) twice is very low.
If you had 103 trillion version 4 UUIDs stored in the database, the probability of generating a duplicated new one is one in a billion. 🤓
For the same reason, if you decided to migrate your database, combine it with another database and mix records, etc. you would most probably be able to just use the same UUIDs you had originally.
Warning
There's still a chance you could have a collision, but it's very low. In most cases you could assume you wouldn't have it, but it would be good to be prepared for it.
UUIDs Prevent Information Leakage¶
Because UUIDs version 4 are random, you could give these IDs to the application users or to other systems, without exposing information about your application.
When using auto-incremented integers for primary keys, you could implicitly expose information about your system. For example, someone could create a new hero, and by getting the hero ID 20
they would know that you have 20 heroes in your system (or even less, if some heroes were already deleted).
UUID Storage¶
Because UUIDs are 16 bytes, they would consume more space in the database than a smaller auto-incremented integer (commonly 4 bytes).
Depending on the database you use, UUIDs could have better or worse performance. If you are concerned about that, you should check the documentation for the specific database.
SQLite doesn't have a specific UUID type, so it will store the UUID as a string. Other databases like Postgres have a specific UUID type which would result in better performance and space usage than strings.
Models with UUIDs¶
To use UUIDs as primary keys we need to import uuid
, which is part of the Python standard library (we don't have to install anything) and use uuid.UUID
as the type for the ID field.
We also want the Python code to generate a new UUID when creating a new instance, so we use default_factory
.
The parameter default_factory
takes a function (or in general, a "callable"). This function will be called when creating a new instance of the model and the value returned by the function will be used as the default value for the field.
For the function in default_factory
we pass uuid.uuid4
, which is a function that generates a new UUID version 4.
Tip
We don't call uuid.uuid4()
ourselves in the code (we don't put the parenthesis). Instead, we pass the function itself, just uuid.uuid4
, so that SQLModel can call it every time we create a new instance.
This means that the UUID will be generated in the Python code, before sending the data to the database.
import uuid
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
# Code below omitted 👇
👀 Full file preview
import uuid
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
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_hero():
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero)
print("The hero ID was already set")
print(hero.id)
session.add(hero)
session.commit()
session.refresh(hero)
print("After saving in the DB")
print(hero)
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
statement = select(Hero).where(Hero.id == hero_id)
selected_hero = session.exec(statement).one()
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
def main() -> None:
create_db_and_tables()
create_hero()
select_hero()
if __name__ == "__main__":
main()
🤓 Other versions and variants
import uuid
from typing import Union
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Union[int, None] = Field(default=None, index=True)
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_hero():
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero)
print("The hero ID was already set")
print(hero.id)
session.add(hero)
session.commit()
session.refresh(hero)
print("After saving in the DB")
print(hero)
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
statement = select(Hero).where(Hero.id == hero_id)
selected_hero = session.exec(statement).one()
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
def main() -> None:
create_db_and_tables()
create_hero()
select_hero()
if __name__ == "__main__":
main()
Pydantic has support for UUID
types.
For the database, SQLModel internally uses SQLAlchemy's Uuid
type.
Create a Record with a UUID¶
When creating a Hero
record, the id
field will be automatically populated with a new UUID because we set default_factory=uuid.uuid4
.
As uuid.uuid4
will be called when creating the model instance, even before sending it to the database, we can access and use the ID right away.
And that same ID (a UUID) will be saved in the database.
# Code above omitted 👆
def create_hero():
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero)
print("The hero ID was already set")
print(hero.id)
session.add(hero)
session.commit()
session.refresh(hero)
print("After saving in the DB")
print(hero)
# Code below omitted 👇
👀 Full file preview
import uuid
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
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_hero():
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero)
print("The hero ID was already set")
print(hero.id)
session.add(hero)
session.commit()
session.refresh(hero)
print("After saving in the DB")
print(hero)
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
statement = select(Hero).where(Hero.id == hero_id)
selected_hero = session.exec(statement).one()
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
def main() -> None:
create_db_and_tables()
create_hero()
select_hero()
if __name__ == "__main__":
main()
🤓 Other versions and variants
import uuid
from typing import Union
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Union[int, None] = Field(default=None, index=True)
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_hero():
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero)
print("The hero ID was already set")
print(hero.id)
session.add(hero)
session.commit()
session.refresh(hero)
print("After saving in the DB")
print(hero)
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
statement = select(Hero).where(Hero.id == hero_id)
selected_hero = session.exec(statement).one()
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
def main() -> None:
create_db_and_tables()
create_hero()
select_hero()
if __name__ == "__main__":
main()
Select a Hero¶
We can do the same operations we could do with other fields.
For example we can select a hero by ID:
# Code above omitted 👆
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
statement = select(Hero).where(Hero.id == hero_id)
selected_hero = session.exec(statement).one()
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
# Code below omitted 👇
👀 Full file preview
import uuid
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
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_hero():
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero)
print("The hero ID was already set")
print(hero.id)
session.add(hero)
session.commit()
session.refresh(hero)
print("After saving in the DB")
print(hero)
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
statement = select(Hero).where(Hero.id == hero_id)
selected_hero = session.exec(statement).one()
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
def main() -> None:
create_db_and_tables()
create_hero()
select_hero()
if __name__ == "__main__":
main()
🤓 Other versions and variants
import uuid
from typing import Union
from sqlmodel import Field, Session, SQLModel, create_engine, select
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Union[int, None] = Field(default=None, index=True)
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_hero():
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero)
print("The hero ID was already set")
print(hero.id)
session.add(hero)
session.commit()
session.refresh(hero)
print("After saving in the DB")
print(hero)
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
statement = select(Hero).where(Hero.id == hero_id)
selected_hero = session.exec(statement).one()
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
def main() -> None:
create_db_and_tables()
create_hero()
select_hero()
if __name__ == "__main__":
main()
Tip
Even if a database like SQLite stores the UUID as a string, we can select and run comparisons using a Python UUID object and it will work.
SQLModel (actually SQLAlchemy) will take care of making it work. ✨
Select with session.get()
¶
We could also select by ID with session.get()
:
# Code above omitted 👆
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
selected_hero = session.get(Hero, hero_id)
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
# Code below omitted 👇
👀 Full file preview
import uuid
from sqlmodel import Field, Session, SQLModel, create_engine
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
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_hero():
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero)
print("The hero ID was already set")
print(hero.id)
session.add(hero)
session.commit()
session.refresh(hero)
print("After saving in the DB")
print(hero)
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
selected_hero = session.get(Hero, hero_id)
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
def main() -> None:
create_db_and_tables()
create_hero()
select_hero()
if __name__ == "__main__":
main()
🤓 Other versions and variants
import uuid
from typing import Union
from sqlmodel import Field, Session, SQLModel, create_engine
class Hero(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
name: str = Field(index=True)
secret_name: str
age: Union[int, None] = Field(default=None, index=True)
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_hero():
with Session(engine) as session:
hero_1 = Hero(name="Deadpond", secret_name="Dive Wilson")
print("The hero before saving in the DB")
print(hero_1)
print("The hero ID was already set")
print(hero_1.id)
session.add(hero_1)
session.commit()
session.refresh(hero_1)
print("After saving in the DB")
print(hero_1)
def select_hero():
with Session(engine) as session:
hero_2 = Hero(name="Spider-Boy", secret_name="Pedro Parqueador")
session.add(hero_2)
session.commit()
session.refresh(hero_2)
hero_id = hero_2.id
print("Created hero:")
print(hero_2)
print("Created hero ID:")
print(hero_id)
selected_hero = session.get(Hero, hero_id)
print("Selected hero:")
print(selected_hero)
print("Selected hero ID:")
print(selected_hero.id)
def main() -> None:
create_db_and_tables()
create_hero()
select_hero()
if __name__ == "__main__":
main()
The same way as with other fields, we could update, delete, etc. 🚀
Run the program¶
If you run the program, you will see the UUID generated in the Python code, and then the record saved in the database with the same UUID.
$ python app.py
// Some boilerplate and previous output omitted 😉
// In SQLite, the UUID will be stored as a string
// other DBs like Postgres have a specific UUID type
CREATE TABLE hero (
id CHAR(32) NOT NULL,
name VARCHAR NOT NULL,
secret_name VARCHAR NOT NULL,
age INTEGER,
PRIMARY KEY (id)
)
// Before saving in the DB we already have the UUID
The hero before saving in the DB
name='Deadpond' secret_name='Dive Wilson' id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') age=None
The hero ID was already set
0e44c1a6-88d3-4a35-8b8a-307faa2def28
// The SQL statement to insert the record uses our UUID
INSERT INTO hero (id, name, secret_name, age) VALUES (?, ?, ?, ?)
('0e44c1a688d34a358b8a307faa2def28', 'Deadpond', 'Dive Wilson', None)
// And indeed, the record was saved with the UUID we created 😎
After saving in the DB
age=None id=UUID('0e44c1a6-88d3-4a35-8b8a-307faa2def28') name='Deadpond' secret_name='Dive Wilson'
// Now we create a new hero (to select it in a bit)
Created hero:
age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador'
Created hero ID:
9d90d186-85db-4eaa-891a-def7b4ae2dab
// And now we select it
Selected hero:
age=None id=UUID('9d90d186-85db-4eaa-891a-def7b4ae2dab') name='Spider-Boy' secret_name='Pedro Parqueador'
Selected hero ID:
9d90d186-85db-4eaa-891a-def7b4ae2dab
Learn More¶
You can learn more about UUIDs in:
- The official Python docs for UUID.
- The Wikipedia for UUID.