|
| 1 | +The two most common types used for primary keys in database tables are integers and UUIDs |
| 2 | +(sometimes referred to as GUIDs). |
| 3 | + |
| 4 | +There are a number of tradeoffs to make when deciding whether to use integers vs. UUIDs, |
| 5 | +including: |
| 6 | + |
| 7 | +* UUIDs don't reveal anything about the number of records in a table |
| 8 | +* UUIDs are practically impossible for an adversary to guess (though you shouldn't rely solely on that for security!) |
| 9 | +* UUIDs are harder to communicate/remember |
| 10 | +* UUIDs may result in worse performance for certain access patterns due to the random ordering |
| 11 | + |
| 12 | +You'll have to decide based on your application which is right for you, but if you want to |
| 13 | +use UUIDs/GUIDs for your primary keys, there are some difficulties to navigate. |
| 14 | + |
| 15 | +## Challenges using UUID-valued primary keys with sqlalchemy |
| 16 | + |
| 17 | +Python has support for UUIDs in the standard library, and most relational databases |
| 18 | +have good support for them as well. |
| 19 | + |
| 20 | +However, if you want a database-agnostic or database-driver-agnostic type, you may run into |
| 21 | +challenges. |
| 22 | + |
| 23 | +In particular, the postgres-compatible UUID type provided by sqlalchemy (`sqlalchemy.dialects.postgresql.UUID`) |
| 24 | +will not work with other databases, and it also doesn't come with a way to set a server-default, meaning that |
| 25 | +you'll always need to take responsibility for generating an ID in your application code. |
| 26 | + |
| 27 | +Even worse, if you try to use the postgres-compatible UUID type simultaneously with both `sqlalchemy` and the |
| 28 | +`encode/databases` package, you may run into issues where queries using one require you to set `UUID(as_uuid=True)`, |
| 29 | +when declaring the column, and the other requires you to declare the table using `UUID(as_uuid=False)`. |
| 30 | + |
| 31 | +Fortunately, sqlalchemy provides a |
| 32 | +[backend-agnostic implementation of GUID type](https://docs.sqlalchemy.org/en/13/core/custom_types.html#backend-agnostic-guid-type) |
| 33 | +that uses the postgres-specific UUID type when possible, and more carefully parses the result to ensure |
| 34 | +`uuid.UUID` isn't called on something that is already a `uuid.UUID` (which raises an error). |
| 35 | + |
| 36 | +For convenience, this package includes this `GUID` type, along with conveniences for setting up server defaults |
| 37 | +for primary keys of this type. |
| 38 | + |
| 39 | +## Using GUID |
| 40 | + |
| 41 | +You can create a sqlalchemy table with a GUID as a primary key using the declarative API like this: |
| 42 | + |
| 43 | +```python hl_lines="" |
| 44 | +{!./src/guid1.py!} |
| 45 | +``` |
| 46 | + |
| 47 | +## Server Default |
| 48 | +If you want to add a server default, it will no longer be backend-agnostic, but |
| 49 | +you can use `fastapi_utils.guid_type.GUID_SERVER_DEFAULT_POSTGRESQL`: |
| 50 | + |
| 51 | +```python |
| 52 | +import sqlalchemy as sa |
| 53 | +from sqlalchemy.ext.declarative import declarative_base |
| 54 | + |
| 55 | +from fastapi_utils.guid_type import GUID, GUID_SERVER_DEFAULT_POSTGRESQL |
| 56 | + |
| 57 | +Base = declarative_base() |
| 58 | + |
| 59 | + |
| 60 | +class User(Base): |
| 61 | + __tablename__ = "user" |
| 62 | + id = sa.Column( |
| 63 | + GUID, |
| 64 | + primary_key=True, |
| 65 | + server_default=GUID_SERVER_DEFAULT_POSTGRESQL |
| 66 | + ) |
| 67 | + name = sa.Column(sa.String, nullable=False) |
| 68 | + related_id = sa.Column(GUID) |
| 69 | +``` |
| 70 | +(Behind the scenes, this is essentially just setting the server-side default to `"gen_random_uuid()"`.) |
| 71 | + |
| 72 | +Note this will only work if you have installed the `pgcrypto` extension |
| 73 | +in your postgres instance. If the user you connect with has the right privileges, this can be done |
| 74 | +by calling the `fastapi_utils.guid_type.setup_guids_postgresql` function: |
| 75 | + |
| 76 | +```python |
| 77 | +{!./src/guid2.py!} |
| 78 | +``` |
| 79 | + |
| 80 | +## Non-Server Default |
| 81 | + |
| 82 | +If you are comfortable having no server default for your primary key column, you can still |
| 83 | +make use of an application-side default (so that `sqlalchemy` will generate a default value when you |
| 84 | +create new records): |
| 85 | + |
| 86 | +```python |
| 87 | +import sqlalchemy as sa |
| 88 | +from sqlalchemy.ext.declarative import declarative_base |
| 89 | + |
| 90 | +from fastapi_utils.guid_type import GUID, GUID_DEFAULT_SQLITE |
| 91 | + |
| 92 | +Base = declarative_base() |
| 93 | + |
| 94 | + |
| 95 | +class User(Base): |
| 96 | + __tablename__ = "user" |
| 97 | + id = sa.Column(GUID, primary_key=True, default=GUID_DEFAULT_SQLITE) |
| 98 | + name = sa.Column(sa.String, nullable=False) |
| 99 | + related_id = sa.Column(GUID) |
| 100 | +``` |
| 101 | + |
| 102 | +`GUID_DEFAULT_SQLITE` is just an alias for the standard library `uuid.uuid4`, |
| 103 | +which could be used in its place. |
0 commit comments