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)
Я хочу:
- Убедитесь, что оба партнера симметрично указываются в качестве супругов друг для друга.
- Избегайте дублирования записей об одном и том же браке.
- Эффективно запрашивать всех супругов человека.
Вот код, который я использую для запроса супругов:
# 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
Однако я столкнулся с трудностями:
- Если запрашиваются
p1
супругов, то в запросе должно содержатьсяp2
, а если запрашиваютсяp2
супругов, то в запросе должно содержатьсяp1
- Оба запроса не симметричны
Вопросы:
- Правильно ли я представляю структуру модели для симметричного представления браков? Если нет, то какие улучшения мне следует внести?
- Как я могу эффективно запросить всех супругов человека, оптимизировав базу данных и обеспечив при этом симметрию?
Я бы улучшил вашу структуру. Я думаю, что 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])]