8š
I would suggest that you do such validation the Django way
by overriding the clean
method of Django Model
class Inspection(models.Model):
...
def clean(self):
if <<<your condition>>>:
raise ValidationError({
'<<<field_name>>>': _('Reason for validation error...etc'),
})
...
...
Note, however, that like Model.full_clean(), a modelās clean() method is not invoked when you call your modelās save() method.
it needs to be called manually to validate modelās data, or you can override modelās save method to make it always call the clean() method before triggering theModel
class save method
Another solution that might help is using GenericRelations,
in order to provide a polymorphic field that relates with more than one table, but it can be the case if these tables/objects can be used interchangeably in the system design from the first place.
4š
As mentionned in comments, the reason that āwith this set up it needs bothā is just that you forgot to add the blank=True
to your FK fields, so your ModelForm
(either custom one or the default generated by the admin) will make the form field required. At the db schema level, you could fill both, either one or none of those FKs, it would be ok since you made those db fields nullable (with the null=True
argument).
Also, (cf my other comments), your may want to check that your really want to FKs to be unique. This technically turns your one to many relationship into a one to one relationship ā youāre only allowed one single āinspectionā record for a given GroupID or SiteId (you canāt have two or more āinspectionā for one GroupId or SiteId). If thatās REALLY what you want, you may want to use an explicit OneToOneField instead (the db schema will be the same but the model will be more explicit and the related descriptor much more usable for this use case).
As a side note: in a Django Model, a ForeignKey field materializes as a related model instance, not as a raw id. IOW, given this:
class Foo(models.Model):
name = models.TextField()
class Bar(models.Model):
foo = models.ForeignKey(Foo)
foo = Foo.objects.create(name="foo")
bar = Bar.objects.create(foo=foo)
then bar.foo
will resolve to foo
, not to foo.id
. So you certainly want to rename your InspectionID
and SiteID
fields to proper inspection
and site
. BTW, in Python, the naming convention is āall_lower_with_underscoresā for anything else than class names and pseudo-constants.
Now for your core question: thereās no specific standard SQL way of enforcing a āone or the otherā constraint at the database level, so itās usually done using a CHECK constraint, which is done in a Django model with the modelās meta āconstraintsā option.
This being said, how constraints are actually supported and enforced at the db level depends on your DB vendor (MySQL < 8.0.16 just plain ignore them for example), and the kind of constraint you will need here will not be enforced at the form or model level validation, only when trying to save the model, so you also want to add validation either at the model level (preferably) or form level validation, in both cases in the (resp.) model or formās clean()
method.
So to make a long story short:
-
first double-check that you really want this
unique=True
constraint, and if yes then replace your FK field with a OneToOneField. -
add a
blank=True
arg to both your FK (or OneToOne) fields - add the proper check constraint in your modelās meta ā the doc is succint but still explicit enough if you know to do complex queries with the ORM (and if you donāt itās time you learn ;-))
- add a
clean()
method to your model that checks your have either one or the other field and raises a validation error else
and you should be ok, assuming your RDBMS respects check constraints of course.
Just note that, with this design, your Inspection
model is a totally useless (yet costly !) indirection ā youād get the exact same features at a lower cost by moving the FKs (and constraints, validation etc) directly into InspectionReport
.
Now there might be another solution ā keep the Inspection model, but put the FK as a OneToOneField on the other end of the relationship (in Site and Group):
class Inspection(models.Model):
id = models.AutoField(primary_key=True) # a pk is always unique !
class InspectionReport(models.Model):
# you actually don't need to manually specify a PK field,
# Django will provide one for you if you don't
# id = models.AutoField(primary_key=True)
inspection = ForeignKey(Inspection, ...)
date = models.DateField(null=True) # you should have a default then
comment = models.CharField(max_length=255, blank=True default="")
signature = models.CharField(max_length=255, blank=True, default="")
class Group(models.Model):
inspection = models.OneToOneField(Inspection, null=True, blank=True)
class Site(models.Model):
inspection = models.OneToOneField(Inspection, null=True, blank=True)
And then you can get all the reports for a given Site or Group with yoursite.inspection.inspectionreport_set.all()
.
This avoids having to add any specific constraint or validation, but at the cost of an additional indirection level (SQL join
clause etc).
Which of those solution would be āthe bestā is really context-dependent, so you have to understand the implications of both and check how you typically use your models to find out which is more appropriate for your own needs. As far as Iām concerned and without more context (or in doubt) Iād rather use the solution with the less indirection levels, but YMMV.
NB regarding generic relations: those can be handy when you really have a lot of possible related models and / or donāt know beforehand which models one will want to relate to your own. This is specially useful for reusable apps (think ācommentsā or ātagsā etc features) or extensible ones (content management frameworks etc). The downside is that it makes querying much heavier (and rather impractical when you want to do manual queries on your db). From experience, they can quickly become a PITA bot wrt/ code and perfs, so better to keep them for when thereās no better solution (and/or when the maintenance and runtime overhead is not an issue).
My 2 cents.
- Django TypeError 'method' object is not subscriptable
- 400 Bad Request While Using `django.test.client`
- Django ā Things to know about sorl-thumbnail
4š
Django has a new (since 2.2) interface for creating DB constraints: https://docs.djangoproject.com/en/3.0/ref/models/constraints/
You can use a CheckConstraint
to enforce one-and-only-one is non-null. I use two for clarity:
class Inspection(models.Model):
InspectionID = models.AutoField(primary_key=True, unique=True)
GroupID = models.OneToOneField('PartGroup', on_delete=models.CASCADE, blank=True, null=True)
SiteID = models.OneToOneField('Site', on_delete=models.CASCADE, blank=True, null=True)
class Meta:
constraints = [
models.CheckConstraint(
check=~Q(SiteID=None) | ~Q(GroupId=None),
name='at_least_1_non_null'),
),
models.CheckConstraint(
check=Q(SiteID=None) | Q(GroupId=None),
name='at_least_1_null'),
),
]
This will only enforce the constraint at the DB level. You will need to validate inputs in your forms or serializers manually.
As a side note, you should probably use OneToOneField
instead of ForeignKey(unique=True)
. Youāll also want blank=True
.
- How to check if a Django user is still logged in from the client side only?
- Celery chain breaks if one of the tasks fail
- Difference between UniqueConstraint vs unique_together ā Django 2.2?
- How to thumbnail static files?
- How to check if a Django arrayfield has data
1š
It might be late to answer your question, but I thought my solution might fit to some other personās case.
I would create a new model, letās call it Dependency
, and apply the logic in that model.
class Dependency(models.Model):
Group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
Site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)
Then I would write the logic to be applicable very explicitly.
class Dependency(models.Model):
group = models.ForeignKey('PartGroup', on_delete=models.CASCADE, null=True, unique=True)
site = models.ForeignKey('Site', on_delete=models.CASCADE, null=True, unique=True)
_is_from_custom_logic = False
@classmethod
def create_dependency_object(cls, group=None, site=None):
# you can apply any conditions here and prioritize the provided args
cls._is_from_custom_logic = True
if group:
_new = cls.objects.create(group=group)
elif site:
_new = cls.objects.create(site=site)
else:
raise ValueError('')
return _new
def save(self, *args, **kwargs):
if not self._is_from_custom_logic:
raise Exception('')
return super().save(*args, **kwargs)
Now you just need to create a single ForeignKey
to your Inspection
model.
In your view
functions, you need to create a Dependency
object and then assign it to your Inspection
record. Make sure that you use create_dependency_object
in your view
functions.
This pretty much makes your code explicit and bug proof. The enforcement can be bypassed too very easily. But the point is that it needs prior knowledge to this exact limitation to be bypassed.
- In Django admin, can I require fields in a model but not when it is inline?
- Hide password field in GET but not POST in Django REST Framework where depth=1 in serializer
0š
I think youāre talking about Generic relations, docs.
Your answer looks similar to this one.
Sometime ago I needed to use Generic relations but I read in a book and somewhere else that the usage should be avoided, I think it was Two Scoops of Django.
I ended up creating a model like this:
class GroupInspection(models.Model):
InspectionID = models.ForeignKey..
GroupID = models.ForeignKey..
class SiteInspection(models.Model):
InspectionID = models.ForeignKey..
SiteID = models.ForeignKey..
Iām not sure if it is a good solution and as you mentioned youād rather not use it, but this is worked in my case.
- Implement roles in django rest framework
- Compacting/minifying dynamic html
- How to force the use of SSL for some URL of my Django Application?