Skip to content

Commit 4fa0205

Browse files
authored
Allow upserting rows with existing_pk even if they do not exist. (#889)
* Allow for __force_save__ in Model.upsert() method to save the models despite they already have a pk set. On integrity Error proceed to update the model, so in worst case two db calls will be made. * Fix coverage * Change implementation to checking if the row exists as postgres leaves hanging invalid transaction on integrity error. On force_save always check if row exists and then save/update (so always two queries).
1 parent f94e507 commit 4fa0205

4 files changed

Lines changed: 157 additions & 1 deletion

File tree

ormar/models/mixins/save_mixin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ async def _upsert_model(
305305
if (
306306
save_all or not instance.pk or not instance.saved
307307
) and not instance.__pk_only__:
308-
await instance.upsert()
308+
await instance.upsert(__force_save__=True)
309309
if relation_field and relation_field.is_multi:
310310
await instance._upsert_through_model(
311311
instance=instance,

ormar/models/model.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ async def upsert(self: T, **kwargs: Any) -> T:
3737
:return: saved Model
3838
:rtype: Model
3939
"""
40+
41+
force_save = kwargs.pop("__force_save__", False)
42+
if force_save:
43+
expr = self.Meta.table.select().where(self.pk_column == self.pk)
44+
row = await self.Meta.database.fetch_one(expr)
45+
if not row:
46+
return await self.save()
47+
return await self.update(**kwargs)
48+
4049
if not self.pk:
4150
return await self.save()
4251
return await self.update(**kwargs)
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import uuid
2+
from typing import Optional
3+
4+
import databases
5+
import pytest
6+
import sqlalchemy
7+
8+
import ormar
9+
from tests.settings import DATABASE_URL
10+
11+
database = databases.Database(DATABASE_URL, force_rollback=True)
12+
metadata = sqlalchemy.MetaData()
13+
14+
15+
class Department(ormar.Model):
16+
class Meta:
17+
database = database
18+
metadata = metadata
19+
20+
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
21+
department_name: str = ormar.String(max_length=100)
22+
23+
24+
class Course(ormar.Model):
25+
class Meta:
26+
database = database
27+
metadata = metadata
28+
29+
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
30+
course_name: str = ormar.String(max_length=100)
31+
completed: bool = ormar.Boolean()
32+
department: Optional[Department] = ormar.ForeignKey(Department)
33+
34+
35+
class Student(ormar.Model):
36+
class Meta:
37+
database = database
38+
metadata = metadata
39+
40+
id: uuid.UUID = ormar.UUID(primary_key=True, default=uuid.uuid4)
41+
name: str = ormar.String(max_length=100)
42+
courses = ormar.ManyToMany(Course)
43+
44+
45+
@pytest.fixture(autouse=True, scope="module")
46+
def create_test_database():
47+
engine = sqlalchemy.create_engine(DATABASE_URL)
48+
metadata.drop_all(engine)
49+
metadata.create_all(engine)
50+
yield
51+
metadata.drop_all(engine)
52+
53+
54+
@pytest.mark.asyncio
55+
async def test_uuid_pk_in_save_related():
56+
async with database:
57+
to_save = {
58+
"department_name": "Ormar",
59+
"courses": [
60+
{
61+
"course_name": "basic1",
62+
"completed": True,
63+
"students": [{"name": "Abi"}, {"name": "Jack"}],
64+
},
65+
{
66+
"course_name": "basic2",
67+
"completed": True,
68+
"students": [{"name": "Kate"}, {"name": "Miranda"}],
69+
},
70+
],
71+
}
72+
department = Department(**to_save)
73+
await department.save_related(follow=True, save_all=True)
74+
department_check = (
75+
await Department.objects.select_all(follow=True)
76+
.order_by(Department.courses.students.name.asc())
77+
.get()
78+
)
79+
to_exclude = {
80+
"id": ...,
81+
"courses": {"id": ..., "students": {"id", "studentcourse"}},
82+
}
83+
assert department_check.dict(exclude=to_exclude) == to_save
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from typing import Optional
2+
3+
import databases
4+
import pytest
5+
import sqlalchemy
6+
7+
import ormar
8+
from tests.settings import DATABASE_URL
9+
10+
database = databases.Database(DATABASE_URL, force_rollback=True)
11+
metadata = sqlalchemy.MetaData()
12+
13+
14+
class Director(ormar.Model):
15+
class Meta:
16+
tablename = "directors"
17+
metadata = metadata
18+
database = database
19+
20+
id: int = ormar.Integer(primary_key=True)
21+
name: str = ormar.String(max_length=100, nullable=False, name="first_name")
22+
last_name: str = ormar.String(max_length=100, nullable=False, name="last_name")
23+
24+
25+
class Movie(ormar.Model):
26+
class Meta:
27+
tablename = "movies"
28+
metadata = metadata
29+
database = database
30+
31+
id: int = ormar.Integer(primary_key=True)
32+
name: str = ormar.String(max_length=100, nullable=False, name="title")
33+
year: int = ormar.Integer()
34+
profit: float = ormar.Float()
35+
director: Optional[Director] = ormar.ForeignKey(Director)
36+
37+
38+
@pytest.fixture(autouse=True, scope="module")
39+
def create_test_database():
40+
engine = sqlalchemy.create_engine(DATABASE_URL)
41+
metadata.drop_all(engine)
42+
metadata.create_all(engine)
43+
yield
44+
metadata.drop_all(engine)
45+
46+
47+
@pytest.mark.asyncio
48+
async def test_updating_selected_columns():
49+
async with database:
50+
director1 = await Director(name="Peter", last_name="Jackson").save()
51+
52+
await Movie(
53+
id=1, name="Lord of The Rings", year=2003, director=director1, profit=1.212
54+
).upsert()
55+
56+
with pytest.raises(ormar.NoMatch):
57+
await Movie.objects.get()
58+
59+
await Movie(
60+
id=1, name="Lord of The Rings", year=2003, director=director1, profit=1.212
61+
).upsert(__force_save__=True)
62+
lotr = await Movie.objects.get()
63+
assert lotr.year == 2003
64+
assert lotr.name == "Lord of The Rings"

0 commit comments

Comments
 (0)