diff --git a/docs/models/methods.md b/docs/models/methods.md index 9dc722b..3b81559 100644 --- a/docs/models/methods.md +++ b/docs/models/methods.md @@ -88,7 +88,7 @@ await track.save() # will raise integrity error as pk is populated ## 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. @@ -101,6 +101,42 @@ track = await Track.objects.get(name='The Bird') 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(**kwargs) -> self` diff --git a/docs/releases.md b/docs/releases.md index 5ffe249..e22082c 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -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`. 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. +* `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, 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. diff --git a/ormar/models/model.py b/ormar/models/model.py index 4a2e81f..568a787 100644 --- a/ormar/models/model.py +++ b/ormar/models/model.py @@ -204,7 +204,7 @@ class Model(ModelRow): ) 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. 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. + :param _columns: list of columns to update, if None all are updated + :type _columns: List :raises ModelPersistenceError: If the pk column is not set :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.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) expr = self.Meta.table.update().values(**self_fields) expr = expr.where(self.pk_column == getattr(self, self.Meta.pkname)) diff --git a/tests/test_model_methods/test_update.py b/tests/test_model_methods/test_update.py new file mode 100644 index 0000000..391baf7 --- /dev/null +++ b/tests/test_model_methods/test_update.py @@ -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