1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341
|
# Fields
## The `id` field
The [`ObjectId` data type](https://docs.mongodb.com/manual/reference/method/ObjectId/){:target=blank_}
is the default primary key type used by MongoDB. An `ObjectId` comes with many
information embedded into it (timestamp, machine identifier, ...). Since by default,
MongoDB will create a field `_id` containing an `ObjectId` primary key, ODMantic will
bind it automatically to an implicit field named `id`.
```python hl_lines="9 10" linenums="1"
--8<-- "fields/objectid.py"
```
!!! info "ObjectId creation"
This `id` field will be generated on instance creation, before saving the instance
to the database. This helps to keep consistency between the instances persisted to
the database and the ones only created locally.
Even if this behavior is convenient, it is still possible to [define custom primary
keys](#primary-key).
## Field types
### Optional fields
By default, every single field will be required. To specify a field as non-required, the
easiest way is to use the `typing.Optional` generic type that will allow the field to
take the `None` value as well (it will be stored as `null` in the database) and to give
it a default value of `None`.
```python hl_lines="8" linenums="1"
--8<-- "fields/optional.py"
```
### Union fields
As explained in the [Python Typing
documentation](https://docs.python.org/3/library/typing.html#typing.Optional){:target=bank_},
`Optional[X]` is equivalent to `Union[X, None]`. That implies that the field type will
be either `X` or `None`.
It's possible to combine any kind of type using the `typîng.Union` type constructor. For
example if we want to allow both `string` and `int` in a field:
```python hl_lines="7" linenums="1"
--8<-- "fields/union.py"
```
!!! question "NoneType"
Internally python describes the type of the `None` object as `NoneType` but in
practice, `None` is used directly in type annotations ([more details](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#optional-types-and-the-none-type){:target=bank_}).
### Enum fields
To define choices, it's possible to use the standard `enum` classes:
```python hl_lines="6-8 13" linenums="1"
--8<-- "fields/enum.py"
```
!!! abstract "Resulting documents in the collection `tree` after execution"
```json hl_lines="7"
{ "_id" : ObjectId("5f818f2dd5708527282c49b6"), "kind" : "big", "name" : "Sequoia" }
{ "_id" : ObjectId("5f818f2dd5708527282c49b7"), "kind" : "small", "name" : "Spruce" }
```
If you try to use a value not present in the allowed choices, a [ValidationError](https://docs.pydantic.dev/latest/usage/models/#error-handling){:target=blank_} exception will be raised.
!!! warning "Usage of `enum.auto`"
If you might add some values to an `Enum`, it's strongly recommended not to use the
`enum.auto` value generator. Depending on the order you add choices, it could
completely break the consistency with documents stored in the database.
??? example "Unwanted behavior example"
```python hl_lines="11-12" linenums="1"
--8<-- "fields/inconsistent_enum_1.py"
```
```python hl_lines="6 12-15" linenums="1"
--8<-- "fields/inconsistent_enum_2.py"
```
### Container fields
#### List
```python linenums="1"
--8<-- "fields/container_list.py"
```
!!! tip
It's possible to define element count constraints for a list field using the
[Field][odmantic.field.Field] descriptor.
#### Tuple
```python linenums="1"
--8<-- "fields/container_tuple.py"
```
#### Dict
!!! tip
For mapping types with already known keys, you can see the [embedded models
section](modeling.md#embedded-models).
```python linenums="1"
--8<-- "fields/container_dict.py"
```
!!! tip "Performance tip"
Whenever possible, try to avoid mutable container types (`List`, `Set`, ...) and
prefer their Immutable alternatives (`Tuple`, `FrozenSet`, ...). This will allow
ODMantic to speedup database writes by only saving the modified container fields.
### `BSON` types integration
ODMantic supports native python BSON types ([`bson`
package](https://api.mongodb.com/python/current/api/bson/index.html){:target=blank_}).
Those types can be used directly as field types:
- [`bson.ObjectId`](https://api.mongodb.com/python/current/api/bson/objectid.html){:target=blank_}
- [`bson.Int64`](https://api.mongodb.com/python/current/api/bson/int64.html){:target=blank_}
- [`bson.Decimal128`](https://api.mongodb.com/python/current/api/bson/decimal128.html){:target=blank_}
- [`bson.Regex`](https://api.mongodb.com/python/current/api/bson/regex.html){:target=blank_}
- [`bson.Binary`](https://api.mongodb.com/python/current/api/bson/binary.html#bson.binary.Binary){:target=blank_}
??? info "Generic python to BSON type map"
| Python type | BSON type | Comment |
| ---------------------- | :--------: | ------------------------------------------------------------ |
| `bson.ObjectId` | `objectId` |
| `bool` | `bool` | |
| `int` | `int` | value between -2^31 and 2^31 - 1 |
| `int` | `long` | value not between -2^31 and 2^31 - 1 |
| `bson.Int64` | `long` |
| `float` | `double` |
| `bson.Decimal128` | `decimal` | |
| `decimal.Decimal` | `decimal` |
| `str` | `string` |
| `typing.Pattern` | `regex` |
| `bson.Regex` | `regex` |
| `bytes` | `binData` |
| `bson.Binary` | `binData` |
| `datetime.datetime` | `date` | microseconds are truncated, only naive datetimes are allowed |
| `typing.Dict` | `object` |
| `typing.List` | `array` |
| `typing.Sequence` | `array` |
| `typing.Tuple[T, ...]` | `array` |
### Pydantic fields
Most of the types supported by pydantic are supported by ODMantic. See [pydantic:
Field Types](https://docs.pydantic.dev/latest/usage/types/types/){:target=bank_} for more
field types.
Unsupported fields:
- `typing.Callable`
Fields with a specific behavior:
- `datetime.datetime`: Only [naive datetime
objects](https://docs.python.org/3/library/datetime.html#determining-if-an-object-is-aware-or-naive){:target=blank_}
will be allowed as MongoDB doesn't store the timezone information. Also, the
microsecond information will be truncated.
## Customization
The field customization can mainly be performed using the [Field][odmantic.field.Field]
descriptor. This descriptor is here to define everything about the field except its
type.
### Default values
The easiest way to set a default value to a field is by assigning this default value
directly while defining the model.
```python hl_lines="6" linenums="1"
--8<-- "fields/default_value.py"
```
You can combine default values and an existing [Field][odmantic.field.Field]
descriptor using the `default` keyword argument.
``` python hl_lines="6" linenums="1"
--8<-- "fields/default_value_field.py"
```
!!! info "Default factory"
You may as well define a factory function instead of a value using the
`default_factory` argument of the [Field][odmantic.field.Field] descriptor.
By default, the default factories won't be used while parsing MongoDB documents.
It's possible to enable this behavior with the `parse_doc_with_default_factories`
[Config](modeling.md#advanced-configuration) option.
!!! warning "Default values validation"
Currently the default values are not validated yet during the model creation.
An inconsistent default value might raise a
[ValidationError](https://docs.pydantic.dev/latest/usage/models/#error-handling){:target=blank_}
while building an instance.
### Document structure
By default, the MongoDB documents fields will be named after the field name. It is
possible to override this naming policy by using the `key_name` argument in the
[Field][odmantic.field.Field] descriptor.
{{ async_sync_snippet("fields", "custom_key_name.py", hl_lines="5") }}
!!! abstract "Resulting documents in the collection `player` after execution"
```json hl_lines="3"
{
"_id": ObjectId("5ed50fcad11d1975aa3d7a28"),
"username": "Jack",
}
```
See [this section](#the-id-field) for more details about the `_id` field that has been added.
### Primary key
While ODMantic will by default populate the `id` field as a primary key, you can use any
other field as the primary key.
{{ async_sync_snippet("fields", "custom_primary_field.py", hl_lines="5") }}
!!! abstract "Resulting documents in the collection `player` after execution"
```json
{
"_id": "Leeroy Jenkins"
}
```
!!! info
The Mongo name of the primary field will be enforced to `_id` and you will not be
able to change it.
!!! warning
Using mutable types (Set, List, ...) as primary field might result in inconsistent
behaviors.
### Indexed fields
You can define an index on a single field by using the `index` argument of the
[Field][odmantic.field.Field] descriptor.
More details about index creation can be found in the
[Indexes](modeling.md#indexes) section.
{{ async_sync_snippet("fields", "indexed_field.py", hl_lines="6 10") }}
!!! warning
When using indexes, make sure to call the `configure_database` method
([AIOEngine.configure_database][odmantic.engine.AIOEngine.configure_database] or
[SyncEngine.configure_database][odmantic.engine.SyncEngine.configure_database]) to
persist the indexes to the database.
### Unique fields
In the same way, you can define unique constrains on a single field by using the
`unique` argument of the [Field][odmantic.field.Field] descriptor. This will ensure that
values of this fields are unique among all the instances saved in the database.
More details about unique index creation can be found in the
[Indexes](modeling.md#indexes) section.
{{ async_sync_snippet("fields", "unique_field.py", hl_lines="5 9 15-18") }}
!!! warning
When using indexes, make sure to call the `configure_database` method
([AIOEngine.configure_database][odmantic.engine.AIOEngine.configure_database] or
[SyncEngine.configure_database][odmantic.engine.SyncEngine.configure_database]) to
persist the indexes to the database.
## Validation
As ODMantic strongly relies on pydantic when it comes to data validation, most of the
validation features provided by pydantic are available:
- Add field validation constraints by using the [Field descriptor][odmantic.field.Field]
```python linenums="1"
--8<-- "fields/validation_field_descriptor.py"
```
- Use strict types to prevent to coercion from compatible types ([pydantic: Strict Types](https://docs.pydantic.dev/latest/usage/types/strict_types/){:target=blank_})
```python linenums="1"
--8<-- "fields/validation_strict_types.py"
```
- Define custom field validators ([pydantic:
Validators](https://docs.pydantic.dev/latest/usage/validators/){:target=blank_})
```python linenums="1"
--8<-- "fields/custom_field_validators.py"
```
- Define custom model validators: [more details](modeling.md#custom-model-validators)
## Custom field types
Exactly in the same way pydantic allows it, it's possible to define custom field types as well with ODMantic ([Pydantic: Custom data types](https://docs.pydantic.dev/latest/concepts/types/#custom-types){:target=blank_}).
Sometimes, it might be required to customize as well the field BSON serialization. In
order to do this, the field class will have to implement the `__bson__` class method.
```python linenums="1" hl_lines="11-12 20-24 27-29 32"
--8<-- "fields/custom_bson_serialization.py"
```
In this example, we decide to store string data manually encoded in the ASCII encoding.
The encoding is handled in the `__bson__` class method. On top of this, we handle the
decoding by attempting to decode `bytes` object in the `validate` method.
!!! abstract "Resulting documents in the collection `example` after execution"
```json hl_lines="3"
{
"_id" : ObjectId("5f81fa5e8adaf4bf33f05035"),
"field" : BinData(0,"aGVsbG8gd29ybGQ=")
}
```
!!! warning
When using custom bson serialization, it's important to handle as well the data
validation for data retrieved from Mongo. In the previous example it's done by
handling `bytes` objects in the validate method.
|