add _columns to Model.update()

This commit is contained in:
collerek
2021-04-06 11:00:11 +02:00
parent 38a094baf7
commit f615b7d55e
4 changed files with 155 additions and 2 deletions

View File

@ -88,7 +88,7 @@ await track.save() # will raise integrity error as pk is populated
## update ## update
`update(**kwargs) -> self` `update(_columns: List[str] = None, **kwargs) -> self`
You can update models by using `QuerySet.update()` method or by updating your model attributes (fields) and calling `update()` method. You can update models by using `QuerySet.update()` method or by updating your model attributes (fields) and calling `update()` method.
@ -101,6 +101,42 @@ track = await Track.objects.get(name='The Bird')
await track.update(name='The Bird Strikes Again') await track.update(name='The Bird Strikes Again')
``` ```
To update only selected columns from model into the database provide a list of columns that should be updated to `_columns` argument.
In example:
```python
class Movie(ormar.Model):
class Meta:
tablename = "movies"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="title")
year: int = ormar.Integer()
profit: float = ormar.Float()
terminator = await Movie(name='Terminator', year=1984, profit=0.078).save()
terminator.name = "Terminator 2"
terminator.year = 1991
terminator.profit = 0.520
# update only name
await terminator.update(_columns=["name"])
# note that terminator instance was not reloaded so
assert terminator.year == 1991
# but once you load the data from db you see it was not updated
await terminator.load()
assert terminator.year == 1984
```
!!!warning
Note that `update()` does not refresh the instance of the Model, so if you change more columns than you pass in `_columns` list your Model instance will have different values than the database!
## upsert ## upsert
`upsert(**kwargs) -> self` `upsert(**kwargs) -> self`

View File

@ -9,6 +9,8 @@
* `exclude: Union[Set, Dict, None]` -> set/dict of relations to exclude from save, those relation won't be saved even with `follow=True` and `save_all=True`. * `exclude: Union[Set, Dict, None]` -> set/dict of relations to exclude from save, those relation won't be saved even with `follow=True` and `save_all=True`.
To exclude nested relations pass a nested dictionary like: `exclude={"child":{"sub_child": {"exclude_sub_child_realtion"}}}`. The allowed values follow To exclude nested relations pass a nested dictionary like: `exclude={"child":{"sub_child": {"exclude_sub_child_realtion"}}}`. The allowed values follow
the `fields/exclude_fields` (from `QuerySet`) methods schema so when in doubt you can refer to docs in queries -> selecting subset of fields -> fields. the `fields/exclude_fields` (from `QuerySet`) methods schema so when in doubt you can refer to docs in queries -> selecting subset of fields -> fields.
* `Model.update()` method now accepts `_columns: List[str] = None` parameter, that accepts list of column names to update. If passed only those columns will be updated in database.
Note that `update()` does not refresh the instance of the Model, so if you change more columns than you pass in `_columns` list your Model instance will have different values than the database!
* `Model.dict()` method previously included only directly related models or nested models if they were not nullable and not virtual, * `Model.dict()` method previously included only directly related models or nested models if they were not nullable and not virtual,
now all related models not previously visited without loops are included in `dict()`. This should be not breaking now all related models not previously visited without loops are included in `dict()`. This should be not breaking
as just more data will be dumped to dict, but it should not be missing. as just more data will be dumped to dict, but it should not be missing.

View File

@ -204,7 +204,7 @@ class Model(ModelRow):
) )
return update_count return update_count
async def update(self: T, **kwargs: Any) -> T: async def update(self: T, _columns: List[str] = None, **kwargs: Any) -> T:
""" """
Performs update of Model instance in the database. Performs update of Model instance in the database.
Fields can be updated before or you can pass them as kwargs. Fields can be updated before or you can pass them as kwargs.
@ -213,6 +213,8 @@ class Model(ModelRow):
Sets model save status to True. Sets model save status to True.
:param _columns: list of columns to update, if None all are updated
:type _columns: List
:raises ModelPersistenceError: If the pk column is not set :raises ModelPersistenceError: If the pk column is not set
:param kwargs: list of fields to update as field=value pairs :param kwargs: list of fields to update as field=value pairs
@ -233,6 +235,8 @@ class Model(ModelRow):
) )
self_fields = self._extract_model_db_fields() self_fields = self._extract_model_db_fields()
self_fields.pop(self.get_column_name_from_alias(self.Meta.pkname)) self_fields.pop(self.get_column_name_from_alias(self.Meta.pkname))
if _columns:
self_fields = {k: v for k, v in self_fields.items() if k in _columns}
self_fields = self.translate_columns_to_aliases(self_fields) self_fields = self.translate_columns_to_aliases(self_fields)
expr = self.Meta.table.update().values(**self_fields) expr = self.Meta.table.update().values(**self_fields)
expr = expr.where(self.pk_column == getattr(self, self.Meta.pkname)) expr = expr.where(self.pk_column == getattr(self, self.Meta.pkname))

View File

@ -0,0 +1,111 @@
from typing import Optional
import databases
import pytest
import sqlalchemy
import ormar
from tests.settings import DATABASE_URL
database = databases.Database(DATABASE_URL, force_rollback=True)
metadata = sqlalchemy.MetaData()
class Director(ormar.Model):
class Meta:
tablename = "directors"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="first_name")
last_name: str = ormar.String(max_length=100, nullable=False, name="last_name")
class Movie(ormar.Model):
class Meta:
tablename = "movies"
metadata = metadata
database = database
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100, nullable=False, name="title")
year: int = ormar.Integer()
profit: float = ormar.Float()
director: Optional[Director] = ormar.ForeignKey(Director)
@pytest.fixture(autouse=True, scope="module")
def create_test_database():
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.drop_all(engine)
metadata.create_all(engine)
yield
metadata.drop_all(engine)
@pytest.mark.asyncio
async def test_updating_selected_columns():
async with database:
director1 = await Director(name="Peter", last_name="Jackson").save()
director2 = await Director(name="James", last_name="Cameron").save()
lotr = await Movie(
name="LOTR", year=2001, director=director1, profit=1.140
).save()
lotr.name = "Lord of The Rings"
lotr.year = 2003
lotr.profit = 1.212
await lotr.update(_columns=["name"])
# before reload the field has current value even if not saved
assert lotr.year == 2003
lotr = await Movie.objects.get()
assert lotr.name == "Lord of The Rings"
assert lotr.year == 2001
assert round(lotr.profit, 3) == 1.140
assert lotr.director.pk == director1.pk
lotr.year = 2003
lotr.profit = 1.212
lotr.director = director2
await lotr.update(_columns=["year", "profit"])
lotr = await Movie.objects.get()
assert lotr.year == 2003
assert round(lotr.profit, 3) == 1.212
assert lotr.director.pk == director1.pk
@pytest.mark.asyncio
async def test_not_passing_columns_or_empty_list_saves_all():
async with database:
director = await Director(name="James", last_name="Cameron").save()
terminator = await Movie(
name="Terminator", year=1984, director=director, profit=0.078
).save()
terminator.name = "Terminator 2"
terminator.year = 1991
terminator.profit = 0.520
await terminator.update(_columns=[])
terminator = await Movie.objects.get()
assert terminator.name == "Terminator 2"
assert terminator.year == 1991
assert round(terminator.profit, 3) == 0.520
terminator.name = "Terminator 3"
terminator.year = 2003
terminator.profit = 0.433
await terminator.update()
terminator = await terminator.load()
assert terminator.name == "Terminator 3"
assert terminator.year == 2003
assert round(terminator.profit, 3) == 0.433