Hi there,
Long-time lurker / Django user; first-time poster.
Ask: Any other ideas for ensuring that ForeignKey relationships are valid?
Context / background: I'm working on a multitenant app, and looking at ways of ensuring the integrity of the data in the system. Specifically, I want to ensure that for a "tenant'ed" model that contains ForeignKey fields, those FK relations match the same tenant.
Unfortunately CHECK constraints aren't suitable, as these are limited to just the row being inserted/updated, so nested SELECTs or similar at the database level aren't possible. It seems as if a TRIGGER could do the job, but I'm wary of going down this path if there are other solutions that I've missed.
At present I've implemented something along the following simplified lines within the application.
All ideas greatly appreciated. Thanks in advance :)
# models.py
class BaseModel(models.Model):
match_fields: List[List[str]] = []
class Meta:
abstract = True
def clean(self):
self.validate_matching_fields()
return super().clean()
def validate_matching_fields(self) -> None:
for field_names in self.match_fields:
field_values: Dict[str, Any] = dict()
for field_name in field_names:
value = self
for field in field_name.split("."):
value = getattr(value, field)
field_values[field_name] = value
_values = list(field_values.values())
assert len(_values) > 1
values_equal = all([V == _values[0] for V in _values])
if not values_equal: # pragma: no branch
msg = f"One or more required fields not matching: {field_values}."
raise ValidationError(msg)
return
# Tenant model
class Organization(models.Model):
name = models.CharField(max_length=64)
class Author(models.Model):
organization = models.ForeignKey(to=Organization, on_delete=models.CASCADE)
name = models.CharField(max_length=64)
# Target model
# I want to ensure that BlogPost.organization == BlogPost.lead_author.organization
class BlogPost(BaseModel):
match_fields = [["organization.id", "lead_author.organization.id"]]
organization = models.ForeignKey(to=Organization, on_delete=models.CASCADE)
lead_author = models.ForeignKey(to=Author, on_delete=models.CASCADE)
slug = models.SlugField(max_length=64)