Автоматическое построения диаграмм сущность-связь

в 5:19, , рубрики: ER-диаграммы, postgresql, python, sql, Регулярные выражения, метки: , , ,

Я не люблю всякие визуальные редакторы для создания ER-моделей. Предпочитаю писать SQL код вручную, но для всяких отчетов и обсуждений хотелось иметь графическое представление и желательно в векторном формате.

Достичь этого удалось довольно легко на Python с помощью стандартного модуля re и дополнительного gv для связи с библиотекой Graphviz.Начну сразу с результата. Вот как выглядит диаграмма сущность-связь для системы документооборота и учета нагрузки в ВУЗе, точнее часть системы.
Диаграмма сущность-связь для системы документооборота и учета нагрузки в ВУЗе

Основная идея поиска с помошью регулярных выражений состоит вначале в нахождении оператора CREATE TABLE. Он может быть описан с помощью следующий конструкции:

table_re = re.compile(r"CREATEs+TABLEs+([w_.]+)s*(([^;]*));", re.UNICODE)

Затем выделения идентификаторов строк описывающих клонки таблицы:

identifier_re = re.compile(r"s*([w_.]+)s*", re.UNICODE)

При их разборе, также осуществляется поиск конструкций связанных с первичными и внешними ключами.
Также одновременно создается словарь таблиц и связей между ними с учетом конкретных колонок, на которые эти связи указывают.

После построение словаря таблиц и связей между ними с помощью модуля gv для связи с библиотекой Graphviz производится непосредственное построение диаграмм сущность-связь. При построение было обнаружено некорректное построение связей таблиц при работе с русскими названиями. Выход был найден с использованием html кода, укороченную версию которого поддерживает Graphviz.

В результате программа корректно рисует связи между таблицами. Первичный ключ имеет световую подсветку.

Текст самой программы ниже. Разбор с помощью регулярных выражений сделан поверхностно, лишь бы заработало. Он не умеет парсить SQL код содержащий конструкции отличные от CREATE TABLE. Все никак руки не доходят до клавиатуры, чтобы сделать все грамотно. Так что код не очень.

#-*- coding: utf-8 -*-

import re, gv

sql = "".join(open('LP.sql', 'r')).decode('utf-8')

class Table:
  tables = {}

  def __init__(self, name):
    assert name not in Table.tables
    Table.tables[name] = self
    self.name = name
    self.cols = []
    self.primarys = []
    self.references = {}

table_re = re.compile(r"CREATEs+TABLEs+([w_.]+)s*(([^;]*));", re.UNICODE)
identifier_re = re.compile(r"s*([w_.]+)s*", re.UNICODE)
i = 0
while True:
  m = table_re.search(sql)
  if not m:
    break
  sql = sql[len(m.group(0)):]
  tbl = Table(m.group(1).encode('utf-8'))
  i += 1
  tbl.i = i
  sql1 = m.group(2)
  while True:
    m1 = identifier_re.search(sql1)
    if not m1:
      break
    sql1 = sql1[len(m1.group(0)):]
    left, comma = sql1.find('('), sql1.find(',')
    if left >=0 and left < comma:
      comma = sql1.find(',', sql1.find(')', left + 1) + 1)
    if comma > 0:
      tail = sql1[:comma]
      sql1 = sql1[comma:]
    else:
      tail = sql1
      sql1 = ""
    if m1.group(1) not in ['UNIQUE', 'CHECK']:
      tbl.cols.append(m1.group(1).encode('utf-8'))
      f = tail.find('PRIMARY')
      if f >= 0:
        tbl.primarys.append(m1.group(1).encode('utf-8'))
      f = tail.find('REFERENCES')
      if f >= 0:
        m1 = identifier_re.search(tail[f+len('REFERENCES'):])
        assert m1
        tbl.references[tbl.cols[-1]] = m1.group(1).encode('utf-8')

g = gv.graph('LP')
for tbl in Table.tables.itervalues():
  tbl.node = gv.node(g, 'tbl%d' % tbl.i)
  gv.setv(tbl.node, 'shape', 'plaintext')
  lst = []
  for c in tbl.cols:
    if tbl.primarys and c == tbl.primarys[0]:
      lst.append('<TR><TD ALIGN="LEFT" BGCOLOR="LightGoldenrod" PORT="pk">%s</TD></TR>' % c)
    elif c in tbl.references:
      lst.append('<TR><TD ALIGN="LEFT" PORT="fk%d">%s</TD></TR>' % (tbl.cols.index(c), c))
    else:
      lst.append('<TR><TD ALIGN="LEFT">%s</TD></TR>' % c)
  gv.setv(tbl.node, 'label', """<
<FONT FACE="Helvetica">
<TABLE BORDER="0" BGCOLOR="Bisque" CELLBORDER="0" CELLSPACING="0"
    CELLPADDING="1" VALIGN="MIDDLE">
  <TR><TD ALIGN="CENTER" BGCOLOR="LightBlue"><FONT
    FACE="Helvetica Bold">%s</FONT></TD></TR>
  %s
</TABLE></FONT>>""" % (tbl.name, 'n  '.join(lst)))

for tbl in Table.tables.itervalues():
  for c, r in tbl.references.iteritems():
    e = gv.edge(tbl.node, Table.tables[r].node)
    gv.setv(e, 'style', 'solid')
    gv.setv(e, 'dir', 'both')
    gv.setv(e, 'arrowtail', 'dot')
    gv.setv(e, 'arrowhead', 'odiamond')
    gv.setv(e, 'tailport', 'fk%d' % tbl.cols.index(c))
    gv.setv(e, 'headport', 'pk')

gv.layout(g, 'dot')
gv.render(g, 'svg', './LP.svg')
gv.render(g, 'pdf', './LP.pdf')
gv.render(g, 'png', './LP.png')

Если кому интересно ниже исходный SQL код.

CREATE TABLE tblФормыОбучения (
  pk_формаОбучения        SERIAL PRIMARY KEY,
  мнемо                   TEXT UNIQUE NOT NULL,
  название                TEXT UNIQUE NOT NULL
);

CREATE TABLE tblУровниОбучения (
  pk_уровеньОбучения      SERIAL PRIMARY KEY,
  мнемо                   TEXT UNIQUE NOT NULL,
  название                TEXT UNIQUE NOT NULL
);

CREATE TABLE tblПредметы (
  pk_предмет              SERIAL PRIMARY KEY,
  мнемо                   TEXT UNIQUE NOT NULL,
  название                TEXT UNIQUE NOT NULL
);

CREATE TABLE tblРазбиения (
  pk_разбиение            SERIAL PRIMARY KEY,
  мнемо                   TEXT UNIQUE NOT NULL,
  название                TEXT UNIQUE NOT NULL
);

CREATE TABLE tblУчебныеГода (
  pk_учебныйГод           SERIAL PRIMARY KEY,
  мнемо                   TEXT UNIQUE NOT NULL,
  название                TEXT UNIQUE NOT NULL
);

CREATE TABLE tblФакультеты (
  pk_факультет            SERIAL PRIMARY KEY,
  мнемо                   TEXT UNIQUE NOT NULL,
  название                TEXT UNIQUE NOT NULL,
  логин                   TEXT UNIQUE NOT NULL,
  пароль                  TEXT NOT NULL
);

CREATE TABLE tblСпециальности (
  pk_специальность        SERIAL PRIMARY KEY,
  fk_учебныйГод           INTEGER REFERENCES tblУчебныеГода(pk_учебныйГод) NOT NULL,
  fk_уровеньОбучения      INTEGER REFERENCES tblУровниОбучения(pk_уровеньОбучения) NOT NULL,
  номер                   TEXT NOT NULL,
  мнемо                   TEXT NOT NULL,
  название                TEXT NOT NULL,
  стандарт                BYTEA,
  дополнение              HSTORE,
  UNIQUE(fk_учебныйГод, fk_уровеньОбучения, номер),
  UNIQUE(fk_учебныйГод, fk_уровеньОбучения, мнемо),
  UNIQUE(fk_учебныйГод, fk_уровеньОбучения, название)
);

CREATE TABLE tblКафедры (
  pk_кафедра              INTEGER PRIMARY KEY,
  fk_учебныйГод           INTEGER REFERENCES tblУчебныеГода(pk_учебныйГод) NOT NULL,
  fk_факультет            INTEGER REFERENCES tblФакультеты(pk_факультет) NOT NULL,
  мнемо                   TEXT UNIQUE NOT NULL,
  название                TEXT UNIQUE NOT NULL,
  логин                   TEXT UNIQUE NOT NULL,
  пароль                  TEXT NOT NULL,
  дополнение              HSTORE,
  UNIQUE(fk_учебныйГод, fk_факультет, мнемо),
  UNIQUE(fk_учебныйГод, fk_факультет, название)
);

CREATE TABLE tblПотоки (
  pk_поток                SERIAL PRIMARY KEY,
  fk_учебныйГод           INTEGER REFERENCES tblУчебныеГода(pk_учебныйГод) NOT NULL,
  fk_факультет            INTEGER REFERENCES tblФакультеты(pk_факультет) NOT NULL,
  название                TEXT NOT NULL,
  дополнение              HSTORE,
  UNIQUE(fk_учебныйГод, fk_факультет, название)
);

CREATE TABLE tblГруппы (
  pk_группа               SERIAL PRIMARY KEY,
  fk_формаОбучения        INTEGER REFERENCES tblФормыОбучения(pk_формаОбучения) NOT NULL,
  fk_специальность        INTEGER REFERENCES tblСпециальности(pk_специальность) NOT NULL,
  курс                    INTEGER NOT NULL,
  комм                    BOOLEAN NOT NULL,
  колБюджет               INTEGER NOT NULL,
  колКомм                 INTEGER NOT NULL,
  дополнение              HSTORE
) INHERITS (tblПотоки);

CREATE TABLE tblПодгруппы (
  pk_подгруппа            SERIAL PRIMARY KEY,
  fk_группа               INTEGER REFERENCES tblГруппы(pk_группа) NOT NULL,
  fk_разбиение            INTEGER REFERENCES tblРазбиения(pk_разбиение) NOT NULL,
  колБюджет               INTEGER NOT NULL,
  колКомм                 INTEGER NOT NULL,
  дополнение              HSTORE
) INHERITS (tblПотоки);

CREATE TABLE tblПотоковЛес (
  fk_поток                INTEGER REFERENCES tblПотоки(pk_поток),
  fk_надПоток             INTEGER REFERENCES tblПотоки(pk_поток),
  UNIQUE(fk_поток, fk_надПоток)
);

CREATE TABLE tblЗаявки (
  pk_заявка               SERIAL PRIMARY KEY,
  fk_предмет              INTEGER REFERENCES tblПредметы(pk_предмет) NOT NULL,
  fk_поток                INTEGER REFERENCES tblПотоки(pk_поток) NOT NULL,
  fk_кафедра              INTEGER REFERENCES tblКафедры(pk_кафедра),
  семестр                 BOOLEAN NOT NULL,
  УМК                     BYTEA,
  дополнение              HSTORE,
-- аудиторные занятия
  лек                     INTEGER,
  сем                     INTEGER,
  лаб                     INTEGER,
  СРС                     INTEGER,
  "т/конс"                BOOLEAN,
  экз                     BOOLEAN,
  зач                     BOOLEAN,
  "п/зач"                 BOOLEAN,
  контр                   BOOLEAN,
-- руководство
  "курс/раб"              BOOLEAN,
  "дип/бак"               BOOLEAN,
-- дополнительная
  практ                   REAL,
  ГЭК                     INTEGER,
  ГАК                     INTEGER,
  прием                   INTEGER,
  курат                   INTEGER,
  "асп/рук"               INTEGER,
  факул                   INTEGER,
  UNIQUE(fk_предмет, fk_поток, семестр)
);

Также неплохо сделать наследование, но нет времени. Для этой задачи лучше иметь иметь иерархическое представление групп, подгрупп и потоков.

Автор: bya

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js