|
| 1 | +# Use SQLAlchemy columns with `sa_column` |
| 2 | + |
| 3 | +Sometimes you need full control over how a database column is defined — beyond what `Field()` options provide. |
| 4 | + |
| 5 | +SQLModel lets you pass a fully configured SQLAlchemy `Column(...)` using the `sa_column` parameter. |
| 6 | + |
| 7 | +This allows you to use advanced SQLAlchemy features and third‑party column types directly while keeping the simplicity of SQLModel models. |
| 8 | + |
| 9 | +/// info |
| 10 | +`sa_column` provides a low-level hook to supply a complete SQLAlchemy `Column(...)` object for a field. SQLModel will use the column's type, options, and constraints as-is. |
| 11 | +/// |
| 12 | + |
| 13 | +## What `sa_column` enables |
| 14 | + |
| 15 | +- Fine‑grained control over column definitions (e.g. `ForeignKey`, `CheckConstraint`, `UniqueConstraint`, `Index`, `server_default`, `server_onupdate`). |
| 16 | +- Custom/third‑party SQLAlchemy types (for example, encrypted strings, PostgreSQL `JSONB`, etc.). |
| 17 | +- Easier migration from or integration with existing SQLAlchemy models. |
| 18 | + |
| 19 | +## Use case: encrypted field with a custom type |
| 20 | + |
| 21 | +Use a third‑party SQLAlchemy type from `sqlalchemy-utils` to encrypt a string field. The key idea is that the field uses a full SQLAlchemy `Column(...)` via `sa_column`. |
| 22 | + |
| 23 | +/// warning | Deprecation |
| 24 | + |
| 25 | +`EncryptedType` is deprecated in SQLAlchemy‑Utils since version `0.36.6`. Use `StringEncryptedType` instead. |
| 26 | + |
| 27 | +<a href="https://sqlalchemy-utils.readthedocs.io/en/latest/data_types.html#encryptedtype" class="external-link" target="_blank">See the upstream deprecation note</a>. |
| 28 | + |
| 29 | +/// |
| 30 | + |
| 31 | +Note: `StringEncryptedType` provides explicit string type handling and better compatibility with SQLAlchemy 2.x. |
| 32 | + |
| 33 | +{* ./docs_src/advanced/sa_column/tutorial001.py *} |
| 34 | + |
| 35 | +### Key points |
| 36 | + |
| 37 | +- The field uses `sa_column=Column(StringEncryptedType(...))`, which gives full control over the SQLAlchemy column while keeping a SQLModel model. |
| 38 | +- `EncryptedType` is deprecated; the example uses `StringEncryptedType` instead. |
| 39 | +- The type is initialized with keyword args (`key=...`, `engine=...`, `padding=...`) to match the installed package signature and avoid runtime errors. |
| 40 | +- The key is read from an environment variable. Don’t hard‑code secrets; use a secrets manager or environment variables, and ensure the same key is available for decryption. |
| 41 | +- In the DB, the value is stored encrypted (you’ll see ciphertext in the SQL echo and database); in Python it’s transparently decrypted when you access the field. |
| 42 | +- Indexing or filtering on encrypted ciphertext is typically not useful; design queries accordingly. |
| 43 | + |
| 44 | +### Run it |
| 45 | + |
| 46 | +To try the encrypted type example locally: |
| 47 | + |
| 48 | +```bash |
| 49 | +python -m venv .venv |
| 50 | +source .venv/bin/activate |
| 51 | +pip install sqlmodel sqlalchemy-utils cryptography |
| 52 | +export SQLMODEL_ENCRYPTION_KEY="change-me" |
| 53 | + |
| 54 | +# Copy the code from docs_src/advanced/sa_column/tutorial001.py into app.py |
| 55 | +python app.py |
| 56 | +``` |
| 57 | + |
| 58 | +After running, you should see "Database and tables created." and a `database_encrypted.db` SQLite file created in your working directory. |
| 59 | + |
| 60 | +/// tip |
| 61 | +If you change the encryption key between runs, delete `database_encrypted.db` first so existing ciphertext doesn’t fail to decrypt with the new key. |
| 62 | +/// |
| 63 | + |
| 64 | +## Use case: enforcing uniqueness |
| 65 | + |
| 66 | +- Single‑column unique: You can express this using `Field(unique=True)` in SQLModel or directly on the SQLAlchemy `Column(...)` when using `sa_column` for full control (e.g., to set a specific SQL type or name). |
| 67 | +- Composite unique (multiple columns): Prefer the idiomatic SQLAlchemy approach with `__table_args__` and `UniqueConstraint`. |
| 68 | + |
| 69 | +{* ./docs_src/advanced/sa_column/tutorial002.py *} |
| 70 | + |
| 71 | +### Key points |
| 72 | + |
| 73 | +- Single‑column unique can be declared with `Field(unique=True)` (simple case) or on the SQLAlchemy `Column(..., unique=True)` via `sa_column` when you need full control over type/nullable/name. `Field(unique=True)` is shorthand for setting `unique=True` on the underlying SQLAlchemy column. |
| 74 | +- Composite unique constraints across multiple columns use `__table_args__ = (UniqueConstraint(...),)`. Naming the constraint helps during migrations and debugging. |
| 75 | +- Nullability matters: a unique, nullable column can usually store multiple NULLs (DB‑specific). Set `nullable=False` for strict uniqueness. |
| 76 | +- The example uses a separate DB file (`database_unique.db`) to avoid colliding with other tutorials. |
| 77 | +- Attempting to insert a duplicate `email` or the same `(name, secret_name)` pair will raise an integrity error. |
| 78 | + |
| 79 | +### Run it |
| 80 | + |
| 81 | +To try the unique constraints example locally on macOS with bash: |
| 82 | + |
| 83 | +```bash |
| 84 | +python -m venv .venv |
| 85 | +source .venv/bin/activate |
| 86 | +pip install sqlmodel |
| 87 | + |
| 88 | +# Copy the code from docs_src/advanced/sa_column/tutorial002.py into app.py |
| 89 | +python app.py |
| 90 | +``` |
| 91 | + |
| 92 | +After running, you should see the selected rows printed, with a database created at `database_unique.db`. Attempting to insert a duplicate `email` (single‑column unique) or a duplicate pair of `(name, secret_name)` (composite unique) would raise an integrity error. |
| 93 | + |
| 94 | +## Important considerations |
| 95 | + |
| 96 | +- **Prefer** built‑in `Field()` parameters (like `unique=True`, `index=True`, `default=...`) when they are sufficient. |
| 97 | +- **Use** `sa_column` only when you need full SQLAlchemy control over the column. |
| 98 | +- **Avoid conflicts** between `sa_column` and other `Field()` arguments that also affect the underlying column. |
| 99 | +- **Match your backend**: ensure the SQLAlchemy `Column(...)` you pass is compatible with your target database. |
| 100 | +- **PostgreSQL**: import and use types like `JSONB`, `ARRAY`, or `UUID` from `sqlalchemy.dialects.postgresql` when appropriate. |
| 101 | + |
| 102 | +## See also |
| 103 | + |
| 104 | +- SQLAlchemy Column docs: <a href="https://docs.sqlalchemy.org/en/20/core/metadata.html#sqlalchemy.schema.Column" class="external-link" target="_blank">`Column`</a> |
| 105 | + - Advanced SQLModel topics: <a href="./index.md" class="internal-link">Advanced User Guide</a> |
0 commit comments