3đź‘Ť
One could certainly imagine a design where save()
did double duty and handled validation for you. For various reasons (partially summarized in the links here), Django decided to make this a two-step process. So I agree with the consensus you found that trying to shoehorn validation into Model.save()
is an anti-pattern. It runs counter to Django’s design, and will probably cause problems down the road.
You’ve already found the “perfect solution”, which is to use Model.full_clean()
to do the validation. I don’t agree with you that remembering this will be burdensome for developers. I mean, remembering to do anything right can be hard, especially with a large and powerful framework, but this particular thing is straightforward, well documented, and fundamental to Django’s ORM design.
This is especially true when you consider what is actually, provably difficult for developers, which is the error handling itself. It’s not like developers could just do model.validate_and_save()
. Rather, they would have to do:
try:
model.validate_and_save()
except ValidationError:
# handle error - this is the hard part
Whereas Django’s idiom is:
try:
model.full_clean()
except ValidationError:
# handle error - this is the hard part
else:
model.save()
I don’t find Django’s version any more difficult. (That said, there’s nothing stopping you from writing your own validate_and_save
convenience method.)
Finally, I would suggest adding a database constraint for your requirement as well. This is what Django does when you add a constraint that it knows how to enforce at the database level. For example, when you use unique=True
on a field, Django will both create a database constraint and add Python code to validate that requirement. But if you want to create a constraint that Django doesn’t know about you can do the same thing yourself. You would simply write a Migration
that creates the appropriate database constraint in addition to writing your own Python version in clean()
. That way, if there’s a bug in your code and the validation isn’t done, you end up with an uncaught exception (IntegrityError
) rather than corrupted data.