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.

On Thu, Jan 19, 2023 at 1:21 PM Abeed Visram <ab...@avayu.co.uk> wrote:

> 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)
>
> --
> 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+unsubscr...@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
> <https://groups.google.com/d/msgid/django-users/18fe8095-3f46-44e6-b418-49e6a411667cn%40googlegroups.com?utm_medium=email&utm_source=footer>
> .
>

-- 
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+unsubscr...@googlegroups.com.
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-users/CABFHQYynFuRXwpC0SXVCD3VMUwG3VrG2SN%3Diod1QcAWXEP%2B3QA%40mail.gmail.com.

Reply via email to