|
1 | 1 | # Database Constraints |
2 | 2 |
|
3 | | -Database constraints are rules that ensure data integrity and prevent invalid data from being inserted into your database. **SQLModel** supports several types of constraints through **SQLAlchemy**. |
| 3 | +In some cases you might want to enforce rules about your data directly at the **database level**. For example, making sure that a hero's name is unique, or that their age is never negative. |
4 | 4 |
|
5 | | -These constraints are enforced at the **database level**, which means they work regardless of which application is inserting the data. This is particularly important for data consistency and reliability in production systems. |
| 5 | +These rules are called **constraints**, and because they live in the database, they work regardless of which application is inserting the data. This is particularly important for data consistency in production systems. |
6 | 6 |
|
7 | 7 | /// info |
8 | 8 |
|
9 | | -**SQLModel** uses <a href="https://docs.sqlalchemy.org/en/20/core/constraints.html" class="external-link" target="_blank">SQLAlchemy's constraint system</a> under the hood, giving you access to all the powerful constraint options available in SQLAlchemy. |
| 9 | +**SQLModel** uses <a href="https://docs.sqlalchemy.org/en/20/core/constraints.html" class="external-link" target="_blank">SQLAlchemy's constraint system</a> under the hood, so you have access to all the powerful constraint options available in SQLAlchemy. |
10 | 10 |
|
11 | 11 | /// |
12 | 12 |
|
13 | 13 | ## Unique Constraints |
14 | 14 |
|
15 | | -Unique constraints ensure that certain fields or combinations of fields have unique values across all rows in the table. |
| 15 | +Let's say you want to make sure that no two heroes can have the same name. The simplest way to do this is with the `unique` parameter in `Field()`: |
16 | 16 |
|
17 | | -### Single Column Unique Constraints |
| 17 | +{* ./docs_src/advanced/constraints/tutorial001_py310.py ln[4:8] hl[5] *} |
18 | 18 |
|
19 | | -The simplest way to add a unique constraint is using the `unique` parameter in the `Field()` function: |
| 19 | +Now the `name` field must be unique across all heroes. If you try to insert a hero with a name that already exists, the database will raise an error. |
20 | 20 |
|
21 | | -{* ./docs_src/advanced/constraints/tutorial001_py310.py ln[4:8] hl[6] *} |
| 21 | +So two heroes named "Deadpond" and "Spider-Boy" would work fine, but trying to add a second "Deadpond" would fail. |
22 | 22 |
|
23 | | -In this example, the `name` field must be unique across all heroes. If you try to insert a hero with a name that already exists, the database will raise an error. |
| 23 | +## Multi-Column Unique Constraints |
24 | 24 |
|
25 | | -✅ **Valid examples:** |
26 | | -* Two heroes with names "Deadpond" and "Spider-Boy" |
27 | | -* Heroes with the same age but different names |
| 25 | +Sometimes you don't need each individual field to be unique, but you want a **combination** of fields to be unique. For example, you might allow multiple heroes named "Spider-Boy" as long as they have different ages. |
28 | 26 |
|
29 | | -🚫 **Invalid examples:** |
30 | | -* Two heroes both named "Deadpond" |
31 | | -* Inserting a hero with a name that already exists in the database |
32 | | - |
33 | | -### Multi-Column Unique Constraints |
34 | | - |
35 | | -Sometimes you want a combination of fields to be unique, even though each individual field can have duplicate values. You can achieve this using `__table_args__` with `UniqueConstraint`: |
| 27 | +You can do this using `__table_args__` with a `UniqueConstraint`: |
36 | 28 |
|
37 | 29 | {* ./docs_src/advanced/constraints/tutorial002_py310.py ln[5:11] hl[6] *} |
38 | 30 |
|
39 | | -In this example, the combination of `name` and `age` must be unique. This means you can have multiple heroes with the same name (as long as they have different ages), and you can have multiple heroes with the same age (as long as they have different names). |
40 | | - |
41 | | -✅ **Valid examples:** |
42 | | -* "Spider-Boy" aged 16 and "Spider-Boy" aged 25 (same name, different ages) |
43 | | -* "Spider-Boy" aged 16 and "Iron Man" aged 16 (different names, same age) |
44 | | - |
45 | | -🚫 **Invalid examples:** |
46 | | -* Two heroes both named "Spider-Boy" and both aged 16 |
| 31 | +With this setup, "Spider-Boy" aged 16 and "Spider-Boy" aged 25 are both allowed, because the **combination** of name and age is different. But two heroes both named "Spider-Boy" and both aged 16 would be rejected. |
47 | 32 |
|
48 | 33 | /// tip |
49 | 34 |
|
50 | | -You can include as many fields as needed in a `UniqueConstraint`. For example: `UniqueConstraint("name", "age", "team")` would make the combination of all three fields unique. |
| 35 | +You can include as many fields as needed in a `UniqueConstraint`. For example, `UniqueConstraint("name", "age", "team")` would require the combination of all three fields to be unique. |
51 | 36 |
|
52 | 37 | /// |
53 | 38 |
|
54 | 39 | ## Check Constraints |
55 | 40 |
|
56 | | -Check constraints allow you to define custom validation rules using SQL expressions. These are more flexible than basic type validation and can enforce business rules at the database level. |
| 41 | +Check constraints let you define custom validation rules using SQL expressions. This is handy for enforcing business rules, like making sure a hero's age is never negative: |
57 | 42 |
|
58 | 43 | {* ./docs_src/advanced/constraints/tutorial003_py310.py ln[5:11] hl[6] *} |
59 | 44 |
|
60 | | -In this example, the check constraint ensures that the `age` field cannot be negative. The constraint has a name (`age_non_negative`) which makes error messages clearer and allows you to reference it later if needed. |
61 | | - |
62 | | -✅ **Valid examples:** |
63 | | -* Heroes with age 0, 16, 25, 100, etc. |
64 | | -* Any non-negative integer for age |
65 | | - |
66 | | -🚫 **Invalid examples:** |
67 | | -* Heroes with negative ages like -5, -1, etc. |
68 | | - |
69 | | -/// info |
70 | | - |
71 | | -Check constraints can use any valid SQL expression supported by your database. Common examples include: |
| 45 | +Here we're saying that `age` must be greater than or equal to zero. The `name` parameter gives the constraint a descriptive label, which makes error messages much easier to understand. |
72 | 46 |
|
73 | | -- **Range checks:** `age BETWEEN 0 AND 150` |
74 | | -- **String length:** `LENGTH(name) >= 2` |
75 | | -- **Pattern matching:** `email LIKE '%@%'` |
76 | | -- **Value lists:** `status IN ('active', 'inactive', 'pending')` |
77 | | - |
78 | | -/// |
79 | | - |
80 | | -### Naming Check Constraints |
81 | | - |
82 | | -It's a good practice to always give your check constraints descriptive names using the `name` parameter. This makes debugging easier when constraint violations occur: |
83 | | - |
84 | | -```python |
85 | | -CheckConstraint("age >= 0", name="age_non_negative") |
86 | | -CheckConstraint("LENGTH(name) >= 2", name="name_min_length") |
87 | | -CheckConstraint("email LIKE '%@%'", name="email_format") |
88 | | -``` |
| 47 | +So heroes with age 0, 16, or 100 would all be fine, but trying to insert a hero with age -5 would fail. |
89 | 48 |
|
90 | 49 | ## Combining Multiple Constraints |
91 | 50 |
|
92 | | -You can combine different types of constraints in the same table by adding multiple constraint objects to `__table_args__`: |
| 51 | +You can mix different types of constraints in the same model by adding multiple constraint objects to `__table_args__`: |
93 | 52 |
|
94 | 53 | {* ./docs_src/advanced/constraints/tutorial004_py310.py ln[5:15] hl[6:10] *} |
95 | 54 |
|
96 | | -This example combines: |
97 | | -- A unique constraint on the combination of `name` and `age` |
98 | | -- A check constraint ensuring age is non-negative |
99 | | -- A check constraint ensuring name has at least 2 characters |
100 | | - |
101 | | -/// tip |
102 | | - |
103 | | -When combining constraints, remember that **all constraints must be satisfied** for data to be inserted successfully. Design your constraints carefully to ensure they work together and don't create impossible conditions. |
104 | | - |
105 | | -/// |
106 | | - |
107 | | -## Constraint Violation Errors |
108 | | - |
109 | | -When constraints are violated, SQLAlchemy will raise exceptions. It's good practice to handle these in your application: |
110 | | - |
111 | | -```python |
112 | | -from sqlalchemy.exc import IntegrityError |
113 | | - |
114 | | -try: |
115 | | - with Session(engine) as session: |
116 | | - # Trying to insert duplicate data |
117 | | - hero = Hero(name="Deadpond", age=48, secret_name="Dive Wilson") |
118 | | - session.add(hero) |
119 | | - session.commit() |
120 | | -except IntegrityError as e: |
121 | | - print(f"Constraint violation: {e}") |
122 | | - session.rollback() |
123 | | -``` |
124 | | - |
125 | | -## Database Support |
| 55 | +This model has three constraints working together: the combination of `name` and `age` must be unique, age cannot be negative, and the name must be at least 2 characters long. All constraints must be satisfied for data to be inserted successfully. |
126 | 56 |
|
127 | 57 | /// warning |
128 | 58 |
|
129 | | -Not all databases support all types of constraints equally: |
130 | | - |
131 | | -- **SQLite:** Supports unique constraints and basic check constraints, but has limitations with some complex SQL expressions |
132 | | -- **PostgreSQL:** Full support for all constraint types with rich SQL expression support |
133 | | -- **MySQL:** Good support for most constraints, with some syntax differences in check constraints |
134 | | -- **SQL Server:** Full support for all constraint types |
| 59 | +Not all databases support all types of constraints equally. In particular, **SQLite** has limitations with some complex SQL expressions in check constraints. Make sure to test your constraints with your target database. |
135 | 60 |
|
136 | | -Always test your constraints with your target database to ensure compatibility. |
| 61 | +Most other SQL databases like **PostgreSQL** and **MySQL** have full or near-full support. 🎉 |
137 | 62 |
|
138 | 63 | /// |
139 | | - |
140 | | -## Best Practices |
141 | | - |
142 | | -🎯 **Use meaningful constraint names** - This makes debugging easier when violations occur |
143 | | - |
144 | | -🎯 **Combine field-level and table-level constraints** - Use `Field(unique=True)` for simple cases and `__table_args__` for complex ones |
145 | | - |
146 | | -🎯 **Consider performance** - Unique constraints automatically create indexes, which can improve query performance |
147 | | - |
148 | | -🎯 **Handle constraint violations gracefully** - Always wrap database operations in try-catch blocks when constraints might be violated |
149 | | - |
150 | | -🎯 **Document your constraints** - Make sure your team understands what business rules the constraints enforce |
151 | | - |
152 | | -/// info |
153 | | - |
154 | | -Remember that **SQLModel** constraints are implemented using **SQLAlchemy**, so you have access to all the power and flexibility of SQLAlchemy's constraint system. For more advanced use cases, check the <a href="https://docs.sqlalchemy.org/en/20/core/constraints.html" class="external-link" target="_blank">SQLAlchemy constraints documentation</a>. |
155 | | - |
156 | | -/// |
0 commit comments