Multitenant app // Validation / constraints for related fields

26 views
Skip to first unread message

Abeed Visram

unread,
Jan 19, 2023, 8:21:39 AM1/19/23
to Django users
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)

ASAMOAH EMMANUEL

unread,
Jan 19, 2023, 6:52:20 PM1/19/23
to django...@googlegroups.com
One other approach you could consider is using Django's built-in form and model validation. You could create a custom form class that inherits from the built-in Django `ModelForm` and override the `clean` method to perform your tenant check.
For example:

class BlogPostForm(forms.ModelForm):
    def clean(self):
        cleaned_data = super().clean()
        if cleaned_data.get('organization') != cleaned_data.get('lead_author').organization:
            raise forms.ValidationError("Organization of the post and lead author must match.")
        return cleaned_data


You could then use this form in your views to handle the validation before saving the model to the database.
Another approach could be to use Django's `clean_fields()` method to validate that the fields you want to check match. for example:

class BlogPost(models.Model):

    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)
   
    def clean_fields(self, exclude=None):
        super().clean_fields(exclude=exclude)
        if self.organization != self.lead_author.organization:
            raise ValidationError("Organization of the post and lead author must match.")

It's worth noting that this approach will run the validation method on each save, so it may cause a performance hit if you are saving large number of records.
In any case, it is essential to test the different options and measure their performance to decide which one is better for your use case.

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/18fe8095-3f46-44e6-b418-49e6a411667cn%40googlegroups.com.
Reply all
Reply to author
Forward
0 new messages