Django: Как представить и запросить симметричные отношения для семейного дерева?

Я создаю приложение семейного древа на Django, где мне нужно симметрично представлять и запрашивать браки. Каждый брак должен содержать только одну запись, а отношения должны включать обоих партнеров без дублирования данных. Вот соответствующая структура модели:

class Person(models.Model):
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    spouses = models.ManyToManyField(
        'self', through="Marriage", symmetrical=True, related_name="partners"
    )
    

class Marriage(models.Model):
    person1 = models.ForeignKey(Person, on_delete=models.CASCADE, related_name="marriages_as_person1")
    person2 = models.ForeignKey(Person, on_delete=models.CASCADE, related_name="marriages_as_person2")
    start_date = models.DateField(null=True, blank=True)
    end_date = models.DateField(null=True, blank=True)

Я хочу:

  1. Убедитесь, что оба партнера симметрично указываются в качестве супругов друг для друга.
  2. Избегайте дублирования записей об одном и том же браке.
  3. Эффективно запрашивать всех супругов человека.

Вот код, который я использую для запроса супругов:

# Query spouses for a person
p1 = Person.objects.create()
p2 = Person.objects.create()
Marriage.objects.create(person1=p1, person2=p2)

p1.spouses.all()   # Returns list containing p2
p2.spouses.all()   # Returns empty list

Однако я столкнулся с трудностями:

  1. Если запрашиваются p1 супругов, то в запросе должно содержаться p2, а если запрашиваются p2 супругов, то в запросе должно содержаться p1
  2. Оба запроса не симметричны

Вопросы:

  1. Правильно ли я представляю структуру модели для симметричного представления браков? Если нет, то какие улучшения мне следует внести?
  2. Как я могу эффективно запросить всех супругов человека, оптимизировав базу данных и обеспечив при этом симметрию?

Я бы улучшил вашу структуру. Я думаю, что ManyToManyField не следует использовать здесь, возможно, мой вариант будет работать лучше в вашем случае. Кроме того, это гарантирует, что для двух человек будет создана только одна запись о браке.

class Person(models.Model):  
    first_name = models.CharField(max_length=100)  
    last_name = models.CharField(max_length=100)  
  
  
class Marriage(models.Model):  
    person1 = models.ForeignKey(  
        Person,  
        on_delete=models.CASCADE,
        related_name='+',  
    )  
    person2 = models.ForeignKey(  
        Person,  
        on_delete=models.CASCADE,  
        related_name='+',  
    )
    unique_key = models.CharField(max_length=50)  
    start_date = models.DateField(null=True, blank=True)  
    end_date = models.DateField(null=True, blank=True) 
 
    class Meta:  
        constraints = [  
            models.UniqueConstraint(  
                fields=['unique_key'],  
                name='unique_marriage_pair',  
            )  
        ] 
 
    def save(  
            self,  
            force_insert=None,  
            force_update=None,  
            using=None,  
            update_fields=None):  
  
        if not self.pk:  
            self.unique_key = '_'.join(  
                map(str, sorted([self.person1_id, self.person2_id]))  
            )  
        return super().save(  
            force_insert=force_insert,  
            force_update=force_update,  
            using=using,  
            update_fields=update_fields,  
        )

Что позволит вам найти все идентификаторы супругов в одном запросе, например, так:

def get_pids(self, obj: Person) -> list[int]:  
    """Returns a list of IDs of all the spouses of the person,  
    ensuring bidirectional relationships.    
    """
    person_id = obj.id  
    queryset = (  
        Marriage.objects  
        .filter(  
            models.Q(person1_id=person_id)  
            | models.Q(person2_id=person_id),  
        )  
        .values_list('person1_id', 'person2_id')  
    )  
    return list(set().union(*queryset) - {person_id})

Или можно будет сделать выборку для списка переданных идентификаторов в двух запросах, как это (при условии использования Postgres):

from collections.abc import Iterable, Iterator  
from typing import TypeAlias  
  
from django.contrib.postgres.aggregates import ArrayAgg  
  
Result: TypeAlias = Iterator[tuple[int, list[int]]]  
  
  
def get_pids_for_few_persons(person_ids: Iterable[int]) -> Result:  
    def get_queryset(search_key: str, aggregate_key: str):  
        return (  
            Marriage.objects  
            .filter(models.Q(**{search_key + '__in': person_ids}))  
            .values(search_key)  
            .annotate(pids=ArrayAgg(aggregate_key))  
            .values_list(search_key, 'pids')  
        )  
  
    queryset1 = get_queryset(  
        search_key='person1_id',  
        aggregate_key='person2_id',  
    )  
    queryset2 = get_queryset(  
        search_key='person2_id',  
        aggregate_key='person1_id',  
    )  
    data1, data2 = dict(queryset1), dict(queryset2)  
    for person_id in data1.keys() | data2.keys():  
        yield person_id, data1.get(person_id, []) + data2.get(person_id, [])


p1 = Person.objects.create()
p2 = Person.objects.create()
p3 = Person.objects.create()
p4 = Person.objects.create()
Marriage.objects.create(person1=p1, person2=p2)
Marriage.objects.create(person1=p3, person2=p1)

result = list(get_pids((1, 2, 3)))
#  [(1, [2, 3]), (2, [1]), (3, [1])]
Вернуться на верх