Django Таблица родословных - PullRequest
0 голосов
/ 16 июня 2020

Я в процессе создания сайта с Django для моей жены и ее клуба собак. Я видел много проектов в php, но ни одного в python.

Я готов заняться этим в любом направлении!

Я хочу динамически создать такую ​​таблицу : enter image description here

Моя модель выглядит так (упрощенно для вопроса):

class Dogs(models.Model):
    did = models.IntegerField(blank=true, null=true)
    name = models.TextField(db_column='name', blank=true, null=true)
    sire = models.TextField(db_column='Sire', blank=true, null=true) 
    dam =  models.TextField(db_column='Dam', blank=true, null=true)

Я создал несколько разных рекурсивных функций, которые правильно определяют предки ребенка, но у меня возникают проблемы с выводом осмысленным образом.

Пример:

dog = []
def pedigree(did, max):
    con = sqlite3.connect('dog.sqlite3')
    con.row_factory
    cursor = con.execute('SELECT "Call Name", ROUND(((JulianDay("now") - JulianDay("Date of Birth"))/365.25),1), "Sex" FROM dogs where did IS %s ' % did).fetchall()
    for row in cursor:
        name, age, gender = row[0], row[1], row[2]
    sql = "SELECT a.Sire, a.Dam, s.'Registered Name', d.'Registered Name' FROM dogs a INNER JOIN dogs s ON a.Sire = s.did INNER JOIN dogs d ON a.Dam = d.did WHERE a.did = "

    printTree(sql, did, name, 0, max)

    return dog

def printTree(stmt, did, name, N, max):
    rspan = 2**(max-N)

    if (rspan > 1):
        #print('rowspan= ', rspan, name)
        dog.append({'name': name, 'rspan': rspan})
        #dog.append(name)


    if(N < max):
        con = sqlite3.connect('dog.sqlite3').execute(stmt+str(did)).fetchall()
        for row in con:
            s, d, sn, dn = row[0], row[1], row[2], row[3]
        printTree(stmt, s, sn, N+1, max)
        printTree(stmt, d, dn, N+1, max)

rspan был для rowspan, поскольку я пытался продублировать некоторые php проекты. Список собак будет выглядеть так:

{'name': 'Son', 'rspan': 8}
{'name': 'Father', 'rspan': 4}
{'name': 'Fathers Grandfather', 'rspan': 2}
{'name': 'Fathers Grandmother', 'rspan': 2}
{'name': 'Mother', 'rspan': 4}
{'name': 'Mothers Grandfather', 'rspan': 2}
{'name': 'Mothers Grandmother', 'rspan': 2}

Я был бы очень признателен за любое руководство, которое могло бы помочь! Спасибо за уделенное время!

1 Ответ

1 голос
/ 16 июня 2020

У меня еще нет полного ответа, но я работаю над той же проблемой, что и перенос существующего приложения 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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...