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 342 343 344 345
|
I'm happy to introduce to you the new version of Beanie and a lot of new features, that come with it.
Here is the feature list:
- Relations
- Event-based actions
- Cache
- Revision
## Relations
This feature is perhaps the most anticipated of all. It took some time, but finally, it is here. Relations.
The document can contain links to other documents in their fields.
*Only top-level fields are fully supported for now.*
Direct link to the document:
```python
from beanie import Document, Link
class Door(Document):
height: int = 2
width: int = 1
class House(Document):
name: str
door: Link[Door] # This is the link
```
List of the links:
```python
from typing import List
from beanie import Document, Link
class Window(Document):
x: int = 10
y: int = 10
class House(Document):
name: str
door: Link[Door]
windows: List[Link[Window]] # This is the list of the links
```
Other link patterns are not supported for now. If you need something more specific for your use-case, please leave an issue on the GitHub page - <https://github.com/roman-right/beanie>
### Write
The next write methods support relations:
- `insert(...)`
- `replace(...)`
- `save(...)`
To apply the writing method to the linked documents, you should set the respective `link_rule` parameter
```python
house.windows = [Window(x=100, y=100)]
house.name = "NEW NAME"
# The next call will insert a new window object
# and replace the house instance with updated data
await house.save(link_rule=WriteRules.WRITE)
# `insert` and `replace` methods will work the same way
```
Or Beanie can ignore internal links with the `link_rule` parameter `WriteRules.DO_NOTHING`
```python
house.door.height = 3
house.name = "NEW NAME"
# The next call will just replace the house instance
# with new data, but the linked door object will not be synced
await house.replace(link_rule=WriteRules.DO_NOTHING)
# `insert` and `save` methods will work the same way
```
### Fetch
#### Prefetch
You can fetch linked documents on the find query step, using the parameter `fetch_links`
```python
houses = await House.find(
House.name == "test",
fetch_links=True
).to_list()
```
All the find methods supported:
- find
- find_one
- get
Beanie uses a single aggregation query under the hood to fetch all the linked documents. This operation is very effective.
#### On-demand fetch
If you don't use prefetching, linked documents will be presented as objects of the `Link` class. You can fetch them manually then.
To fetch all the linked documents you can use the `fetch_all_links` method
```python
await house.fetch_all_links()
```
It will fetch all the linked documents and replace `Link` objects with them.
Or you can fetch a single field:
```python
await house.fetch_link(House.door)
```
This will fetch the Door object and put it in the `door` field of the `house` object.
### Delete
Delete method works the same way as write operations, but it uses other rules:
To delete all the links on the document deletion you should use the `DeleteRules.DELETE_LINKS` value for the `link_rule` parameter
```python
await house.delete(link_rule=DeleteRules.DELETE_LINKS)
```
To keep linked documents you can use the `DO_NOTHING` rule
```python
await house.delete(link_rule=DeleteRules.DO_NOTHING)
```
## Event-based actions
You can register methods as pre- or post- actions for document events like `insert`, `replace` and etc.
Currently supported events:
- Insert
- Replace
- SaveChanges
- ValidateOnSave
To register an action you can use `@before_event` and `@after_event` decorators respectively.
```python
from beanie import Insert, Replace
class Sample(Document):
num: int
name: str
@before_event(Insert)
def capitalize_name(self):
self.name = self.name.capitalize()
@after_event(Replace)
def num_change(self):
self.num -= 1
```
It is possible to register action for a list of events:
```python
from beanie import Insert, Replace
class Sample(Document):
num: int
name: str
@before_event([Insert, Replace])
def capitalize_name(self):
self.name = self.name.capitalize()
```
This will capitalize the `name` field value before each document insert and replace
And sync and async methods could work as actions.
```python
from beanie import Insert, Replace
class Sample(Document):
num: int
name: str
@after_event([Insert, Replace])
async def send_callback(self):
await client.send(self.id)
```
## Cache
All the query results could be locally cached.
This feature must be turned on in the `Settings` inner class explicitly.
```python
class Sample(Document):
num: int
name: str
class Settings:
use_cache = True
```
Beanie uses LRU cache with expiration time. You can set `capacity` (the maximum number of the cached queries) and expiration time in the `Settings` inner class.
```python
class Sample(Document):
num: int
name: str
class Settings:
use_cache = True
cache_expiration_time = datetime.timedelta(seconds=10)
cache_capacity = 5
```
Any query will be cached for this document class.
```python
# on the first call it will go to the database
samples = await Sample.find(num>10).to_list()
# on the second - it will use cache instead
samples = await Sample.find(num>10).to_list()
await asyncio.sleep(15)
# if the expiration time was reached
# it will go to the database again
samples = await Sample.find(num>10).to_list()
```
## Revision
This feature helps with concurrent operations.
It stores `revision_id` together with the document and changes it on each document update. If the application with the old local copy of the document will try to change it, an exception will be raised. Only when the local copy will be synced with the database, the application will be allowed to change the data. It helps to avoid losses of data.
This feature must be turned on in the `Settings` inner class explicitly too.
```python
class Sample(Document):
num: int
name: str
class Settings:
use_revision = True
```
Any changing operation will check if the local copy of the document has the actual `revision_id` value:
```python
s = await Sample.find_one(Sample.name="TestName")
s.num = 10
# If a concurrent process already changed the doc,
# the next operation will raise an error
await s.replace()
```
If you want to ignore revision and apply all the changes even if the local copy is outdated, you can use the parameter `ignore_revision`
```python
await s.replace(ignore_revision=True)
```
## Other
There is a bunch of smaller features, presented in this release. I would like to mention a couple of them here.
### Save changes
Beanie can keep the document state, that synced with the database, to find local changes and save only them.
This feature must be turned on in the `Settings` inner class explicitly.
```python
class Sample(Document):
num: int
name: str
class Settings:
use_state_management = True
```
To save only changed values the `save_changes()` method should be used.
```python
s = await Sample.find_one(Sample.name == "Test")
s.num = 100
await s.save_changes()
```
The `save_changes()` method can be used only with already inserted documents.
### On save validation
Pydantic has very useful config to validate values on assignment - `validate_assignment = True`. But unfortunately, this is a heavy operation and doesn't fit some use cases.
You can validate all the values before saving the document (insert, replace, save, save_changes) with beanie config `validate_on_save` instead.
This feature must be turned on in the `Settings` inner class explicitly.
```python
class Sample(Document):
num: int
name: str
class Settings:
validate_on_save = True
```
If any field has a wrong value, it will raise an error on write operations (insert, replace, save, save_changes)
```python
sample = await Sample.find_one(Sample.name == "Test")
sample.num = "wrong value type"
# Next call will raise an error
await sample.replace()
```
## Conclusion
Thank you for reading. I hope you'll find these features useful.
If you would like to help with development - there are some issues at the GitHub page of the project - <https://github.com/roman-right/beanie>
## Links
- [Beanie Project](https://github.com/roman-right/beanie)
- [Documentation](https://roman-right.github.io/beanie/)
- [Discord Server](https://discord.gg/ZTTnM7rMaz)
|