У меня еще нет полного ответа, но я работаю над той же проблемой, что и перенос существующего приложения PHP на Django. Вы можете увидеть пример родословной здесь . Во-первых, вы можете использовать самосоединения для родителей и объединения для таблиц разведения, питомника и заводчика:
class Dog(SlugMixin, models.Model):
name = models.CharField(max_length=100)
kenn = models.ForeignKey(
'Kennel', models.DO_NOTHING, verbose_name='kennel', db_column='kennel_id',
related_name='dogs')
sex = models.CharField(max_length=1, choices=[
('M', 'Male'), ('F', 'Female')])
birth_year = models.SmallIntegerField(blank=True, null=True)
is_leader = models.BooleanField()
weight = models.SmallIntegerField(blank=True, null=True)
breed = models.ForeignKey(Breed, models.DO_NOTHING, blank=True, null=True)
father = models.ForeignKey(
'self', models.DO_NOTHING, blank=True, null=True, related_name='father_offsprings')
mother = models.ForeignKey(
'self', models.DO_NOTHING, blank=True, null=True, related_name='mother_offsprings')
breeder = models.ForeignKey(
'Musher', models.DO_NOTHING, blank=True, null=True)
....
Затем для получения данных вы можете использовать необработанные запросы, а не курсоры, что дает вам больше структурированный набор запросов при получении данных всего одним запросом (в пределах, я полагаю, 32 соединения в mysql)
Вот функция для генерации необработанного запроса, обратите внимание, что она использует хранимую процедуру для получения информация заводчика:
def get_pedigree_query(max_gen=5):
count = 0
def get_from_clause(str, gen, alias, max_gen):
nonlocal count
count += 1
i = count
alias = alias if i == 1 else f"{alias}{i-1}"
str += f"""
LEFT JOIN dog f{i} ON {alias}.father_id = f{i}.id
LEFT JOIN dog m{i} ON {alias}.mother_id = m{i}.id"""
if gen < max_gen - 1:
str = get_from_clause(str, gen + 1, 'f', max_gen)
str = get_from_clause(str, gen + 1, 'm', max_gen)
return str
_select = "d.id, d.name, d.birth_year, d.is_leader, d.sex, d.slug, d.image_id, d.image_url, getDogBreeder(d.breeder_id) dog_breeder, getDogKennelSlug(d.kennel_id) kennel_slug"
for i in range(1, 2**(max_gen-1)):
_select += f", f{i}.id, f{i}.name, f{i}.birth_year, f{i}.is_leader, f{i}.sex, f{i}.slug, f{i}.image_id, f{i}.image_url, getDogBreeder(f{i}.breeder_id) f_dog_breeder_{i}, getDogKennelSlug(f{i}.kennel_id) f_kennel_slug_{i}, m{i}.id, m{i}.name, m{i}.birth_year, m{i}.is_leader, m{i}.sex, m{i}.slug, m{i}.image_id, m{i}.image_url, getDogBreeder(m{i}.breeder_id) m_dog_breeder_{i}, getDogKennelSlug(m{i}.kennel_id) m_kennel_slug_{i}"
sql = f"SELECT {_select}"
_from = get_from_clause('dog d', 1, 'd', max_gen)
_from += "\ninner join kennel k on d.kennel_id = k.id"
sql += f"\nFROM {_from}"
sql += f"\nWHERE d.kennel_id = %s AND d.slug = %s AND d.is_active = 1 and k.is_active = 1"
return sql
Для собаки, представленной в приведенной выше ссылке, сгенерированный SQL будет:
SELECT d.id, d.name, d.birth_year, d.is_leader, d.sex, d.slug, d.image_id, d.image_url, getDogBreeder(d.breeder_id) dog_breeder, getDogKennelSlug(d.kennel_id) kennel_slug
, f1.id, f1.name, f1.birth_year, f1.is_leader, f1.sex, f1.slug,
f1.image_id, f1.image_url, getDogBreeder(f1.breeder_id) f_dog_breeder_1,
getDogKennelSlug(f1.kennel_id) f_kennel_slug_1, m1.id, m1.name,
m1.birth_year, m1.is_leader, m1.sex, m1.slug, m1.image_id,
m1.image_url, getDogBreeder(m1.breeder_id) m_dog_breeder_1,
getDogKennelSlug(m1.kennel_id) m_kennel_slug_1
, f2.id, f2.name, f2.birth_year, f2.is_leader, f2.sex, f2.slug,
f2.image_id, f2.image_url, getDogBreeder(f2.breeder_id) f_dog_breeder_2,
getDogKennelSlug(f2.kennel_id) f_kennel_slug_2, m2.id, m2.name,
m2.birth_year, m2.is_leader, m2.sex, m2.slug, m2.image_id,
m2.image_url, getDogBreeder(m2.breeder_id) m_dog_breeder_2,
getDogKennelSlug(m2.kennel_id) m_kennel_slug_2
, f3.id, f3.name, f3.birth_year, f3.is_leader, f3.sex, f3.slug,
f3.image_id, f3.image_url, getDogBreeder(f3.breeder_id) f_dog_breeder_3,
getDogKennelSlug(f3.kennel_id) f_kennel_slug_3, m3.id, m3.name,
m3.birth_year, m3.is_leader, m3.sex, m3.slug, m3.image_id,
m3.image_url, getDogBreeder(m3.breeder_id) m_dog_breeder_3,
getDogKennelSlug(m3.kennel_id) m_kennel_slug_3
FROM dog d
LEFT JOIN dog f1 ON d.father_id = f1.id
LEFT JOIN dog m1 ON d.mother_id = m1.id
LEFT JOIN dog f2 ON f1.father_id = f2.id
LEFT JOIN dog m2 ON f1.mother_id = m2.id
LEFT JOIN dog f3 ON m2.father_id = f3.id
LEFT JOIN dog m3 ON m2.mother_id = m3.id
inner join kennel k on d.kennel_id = k.id
WHERE d.kennel_id = %s AND d.slug = %s AND d.is_active = 1 and k.is_active = 1
Например, вы можете получить доступ к прабабушке собаки с помощью:
p = get_pedigree_query(3)
d = Dog.objects.raw(p, [27, 'cypher'])[0]
print(d.mother.mother.mother)
>>>HAWAII-1994 J. Philip, Noatak Kennels
Теперь для печати родословной должно помочь наличие данных в Django объектах ORM. В моем собственном проекте я все еще пытаюсь решить, портирую ли я код из примера приведенной выше ссылки или использую библиотеку JS для генерации графики HTML5 или SVG. Думаю, это может быть дополнительный вопрос.
ОБНОВЛЕНИЕ 2020-06-24 С помощью вышеуказанного решения я столкнулся с ограничением MySQL 61 присоединений для родословных 5+ поколений, и я обнаружил, что MySQL версия 8+ теперь поддерживает рекурсивные запросы CTE. Упрощенный запрос будет выглядеть так:
WITH RECURSIVE ped_cte (
gen,
`path`,
id,
`name`,
slug,
kennel_slug,
father_id,
mother_id
) AS
(SELECT
1 AS gen,
CAST(NULL AS CHAR(255)) AS `path`,
d.id,
d.`name`,
d.slug,
getDogKennelSlug(kennel_id) AS kennel_slug,
d.father_id,
d.mother_id
FROM
dog d
INNER JOIN kennel AS k
ON d.kennel_id = k.id
WHERE k.slug = %s AND d.slug = %s
UNION
ALL
SELECT
gen + 1,
CONCAT_WS('__', `path`, 'father') AS `path`,
f.id,
f.name,
f.slug,
getDogKennelSlug(f.kennel_id) AS kennel_slug,
f.father_id,
f.mother_id
FROM
ped_cte
LEFT OUTER JOIN dog AS f
ON ped_cte.father_id = f.id
WHERE gen <= %s
UNION
ALL
SELECT
gen + 1,
CONCAT_WS('__', `path`, 'mother') AS `path`,
m.id,
m.name,
m.slug,
getDogKennelSlug(m.kennel_id) AS kennel_slug,
m.father_id,
m.mother_id
FROM
ped_cte
LEFT OUTER JOIN dog AS m
ON ped_cte.mother_id = m.id
WHERE gen <= %s)
SELECT
*
FROM
ped_cte;
Запрос строит материализованное поле пути (путь), которое позже позволит нам загружать данные в объект. Обратите внимание, что поле пути было приведено к определенной ширине на первой итерации, чтобы оно сохранялось в рекурсии. Следующим шагом будет выполнение запроса с помощью курсора. Сначала нам нужна вспомогательная функция из документации Django, чтобы возвращать результаты в виде dict:
def dictfetchall(cursor):
"Return all rows from a cursor as a dict"
columns = [col[0] for col in cursor.description]
return [
dict(zip(columns, row))
for row in cursor.fetchall()
]
Затем функция для выполнения запроса с учетом питомника и пули собаки и рекурсивной загрузки данных в объекты отца и матери. Обратите внимание, что поле материализованного пути сохраняется в объекте, поэтому мы сможем использовать его при написании настраиваемого тега шаблона для вывода родословной.
def get_pedigree(kenn_slug, slug, max_gen):
with connection.cursor() as cursor:
cursor.execute(SQL, [kenn_slug, slug, max_gen, max_gen])
dogs = dictfetchall(cursor)
db_dog = next(item for item in dogs if item["path"] == None)
dog = Dog()
for attr, value in db_dog.items():
setattr(dog, attr, value)
def fill_dog(dog, prefix, gen, max_gen):
db_dog = next(
item for item in dogs if item["path"] == prefix + 'father')
father = Dog()
for attr, value in db_dog.items():
setattr(father, attr, value)
setattr(dog, 'father', father)
db_dog = next(
item for item in dogs if item["path"] == prefix + 'mother')
mother = Dog()
for attr, value in db_dog.items():
setattr(mother, attr, value)
setattr(dog, 'mother', mother)
if gen < max_gen:
fill_dog(dog.father, prefix + 'father__', gen+1, max_gen)
fill_dog(dog.mother, prefix + 'mother__', gen+1, max_gen)
return dog
dog = fill_dog(dog, '', 1, max_gen)
return dog