На хабре часто можно встретить различные статьи о том как сделано то или то, с непосредственной реализацией, кодом, примерами, обоснованиями (пусть даже спорными). Кто-то выкладывает пример контролла, кто-то даёт практические советы по яваскрипту. Однако я не видел, чтобы кто-нибудь, рассказывал об организации структуры БД. Дальше каких-то школьных примеров это не заходит (если ошибаюсь поправьте и дайте ссылки). Нет, холивары SQL vs NoSQL меня не интересуют. По моему скромному убеждению — СУБД вторична в вопросах организации БД. Вопросы производительности конкретных СУБД становятся актуальными далеко не сразу. Какая бы ни была выбрана СУБД, под определённую задачу, к производительности предъявляется всего одно требование — производительность должна быть достаточной. А вот пути достижения этой самой достаточности, способы удобно и красиво разместить данные — чтобы быстро и легко их извлекать, организация справочников и индексов, ввода и вывода, способы масштабирования и/или изменения структуры БД в течении жизни, используемые методики, решённые и нерешённые проблемы, полезные рецепты и советы — это всё то, о чём я хочу поговорить.
Разработка структур БД очень интересный и нетривиальный процесс. В этой обширной области встречается мало живых примеров, которые можно посмотреть, обсудить. Неужели вам, разработчики БД, всегда всё ясно что и как делать? Давайте делиться знаниями, давайте спрашивать, рассказывать, обсуждать, узнавать. Какая разница таблица или объект или глобал — важно какой смысл вкладывается, какие связи выстраиваются, какими средствами эти связи реализовываются.
Я не работаю в интерсистемс, это указано в моём профайле только чтобы иметь возможность размещать статьи в их блог (отдельного хаба для MUMPS или COS на хабре нет). Так что описанные мной методы могут не совпадать с «заводскими» рекомендациями использования СУБД Cache и языка Cache Object Script.
Пару дней назад был опубликован перевод, в котором мой подход, к программированию БД, называли экстремальным — я с этим не совсем согласен. В комментариях, было как минимум три человека (Ogoun uaoleg 4dmonster), которые сказали, что им было бы интересно посмотреть на живое использование MUMPS и узнать почему не надо бояться глобалов. Для этих людей и всех тех, кому интересно обсудить затронутые мной темы, я и пишу данную статью.
Определение:
Справочник это медленно меняющийся перечень уникальных позиций, содержащий краткие и точные сведения научного, производственного или прикладного характера, объединенных единой тематикой. Например адресные справочники (страны, города, улицы ...) В определение википедии я не случайно добавил «медленно меняющийся».
Требования:
Основные требования предоставляемые к справочнику:
- Операция получения названия элемента справочника (retrieve) должна выполнятся быстро.
- Название любого элемента справочника можно изменить в одном месте, и это изменение применится во всей системе.
Рассмотрим более подробно:
Быстро получить название элемента справочника — означает не искать. То есть просто прочитать в известном месте и выдать. Это говорит о том, что, к медленно меняющейся информации обращаются часто. Ответ надо выдавать быстро. Как это делается в Cache я покажу далее. Если какой-то там специализированный поиск (найти все города на букву А удалённые от города N не более чем на x км) — ещё может себе позволить кушать процессорное время — то выдача названия элемента справочника — нет.
Изменение названия элемента справочника в одном месте означает что операция updateName по сложности и времени выполнения, аналогична операции retrieve, за исключением случаев, когда требуется проверка на допустимость нового имени. Но даже в этом случае никакая перестройка и переиндексация больших наборов данных, в которых используется данный элемент справочника, не требуется. Это логично вытекает из одной простой особенности любых справочников и вообще данных — в названиях могут быть ошибки. То есть вы можете спроектировать разработать и запустить систему, любой степени сложности и нужности, а через какое-то время, окажется, что допущена ошибка в названии. Вам надо эту ошибку исправить. Вы не хотите проверять и долго переиндексировать всю, или большую часть, вашей системы для исправления этой ошибки. Вы не хотите чтобы старые/новые аналитические/оперативные отчёты или статистика — перестали согласовываться друг с другом из-за изменения одного названия. Вы не хотите заново генерировать seo и прочие шаблоны страниц вашего сайта. Вы хотите изменить одно название в одном месте и забыть об этом. Конечно, большинству разработчиков БД, наверное, это моё требование, покажется очевидным, и не заслуживающим такого большого количества букв. Но оказалось, на практике, оно выполняется значительно реже, чем можно себе представить.
Дополнительные требования предоставляемые к справочнику:
- Необходимо иметь возможность хранить названия элементов справочника на различных языках соблюдая при этом предыдущие требования. Операция retrieve для любого нового языка должна выполнятся также быстро.
- Необходимо хранить историю изменения элементов справочника (названий, структурных позиций, и прочих характеристик) с возможностью проследить и выдавать информацию зависимую от времени t (как этот город назывался вчера, или t лет назад).
Эти требования названы дополнительными, потому что не для всех БД они важны и нужны. К примеру возможность хранить названия элементов на различных языках требуется не всем. Однако, даже если, ваш сайт, заточен под один конкретный язык или регион, не спешите вычеркивать эту функциональность из списка требований — возможно она пригодится и вам. Урлы некоторых страниц содержат в себе латинское название тех или иных элементов справочника. Если это название, каждый раз будет генерится на основании справочного названия — то при updateName — вы можете получить новый урл для старой страницы, только из-за исправления ошибки в названии.
Этих проблем можно избежать задавая название элемента справочника на нескольких языках. Пусть первый будет ru а второй partUri. Тогда при изменении названия на одном языке, оно автоматически не изменится на другом (как минимум этим можно управлять). И адрес страницы останется прежним.
Хранение истории изменений элементов справочника — функциональность, которая требуется не всегда, реализовывается она чуть сложнее, чем многоязычность или изменение имени. Эта функциональность влечёт за собой увеличение времени, требуемого на изменение элемента справочника. Однако, при грамотной реализации, это время увеличивается незначительно. Также, исходя из того, что справочник, это медленно меняющаяся информация, ничего страшного в более длительном изменении элемента нет.
Реализация
Пусть все элементы справочника хранятся в глобале ^Dictionary
Глобал — это глобальная переменная, изменения в которой сохраняются на диске. Индексы переменной идут в скобках после имени переменной через запятую. Например:
^ГлобальнаяПеременная(«индекс1»,«индекс2»,...,«индексN»)=«значение»
Индексы и значения берутся в кавычки только если это не числа. Индексом может быть также другая переменная (подробнее об этом позже).
Примем что индексы глобала ^Dictionary будут означать следующее:
- онтология (грубая классификация справочников) — наша онтология Vehicle (транспортные средства)
- название справочника — TransmissionType (тип трансмиссии)
- идентификатор элемента справочника (причём все идентификаторы уникальны, даже в пределах различных справочников и онтологий)
- номер версии элемента (0-текущая актуальная версия, остальные — история)
- название свойства элемента
Имя глобала и смысл, вкладываемый в каждый индекс придуманы мной (разработчиком). Описание, этих самостоятельных правил я приведу позже (возможно в следующих статьях). На данный момент мы просто посмотрим как может быть организован простейший одноуровневый справочник, безо всякой вложенности и прочего. Данные приводящиеся в примере взяты из реальной живой использующейся БД. Комманда zw выводит значения переменной(глобальной или локальной) со всеми определёнными индексами. Команды будем выполнять в терминале. MONTOLOGY> это название пространства имён (определено и придумано мной ранее). Выведем информацию по нашему справочнику — выполним команду:
zw ^Dictionary(«Vehicle»,«TransmissionType») и посмотрим на результат:
MONTOLOGY>zw ^Dictionary("Vehicle","TransmissionType")
^Dictionary("Vehicle","TransmissionType",1,0,"UpdateTime")="62086,66625"
^Dictionary("Vehicle","TransmissionType",1,0,"uid")=888
^Dictionary("Vehicle","TransmissionType",2,0,"UpdateTime")="62086,66625"
^Dictionary("Vehicle","TransmissionType",2,0,"uid")=888
MONTOLOGY>
Давайте внимательно посмотрим на то что мы вывели. Итак мы распечатали все элементы справочника TransmissionType онтологии Vehicle. Как видно в данном справочнике всего два элемента с идентификаторами 1 и 2. Также очевидно, раз 4й индекс — только 0 — то все элементы этого справочника актуальны, и после добавления ни разу не изменялись (истории нет). У каждого элемента справочника есть всего два свойства: UpdateTime (дата и время обновления в формате Cache) и uid (идентификатор пользователя сделавшего изменение). Ещё раз обратите внимание на практически полное отсутствие служебных слов и символов в команде и результате.
Как видим в нашем справочнике не хватает чего-то важного — а именно названий. Пусть названия всех элементов всех справочников на всех языках хранятся в глобале ^NameDictionaryElement
Примем что индексы глобала ^NameDictionaryElement будут означать следующее:
- идентификатор элемента справочника
- язык
- номер версии названия (0-текущая актуальная версия, остальные — история)
- название свойства (у нас будет использоваться только updateTime)
Выведем информацию по интересующим нас названиям элементов — выполним команду:
zw ^NameDictionaryElement(1),^NameDictionaryElement(2) эта команда аналогична двум последовательно выполненным коммандам:
zw ^NameDictionaryElement(1) и zw ^NameDictionaryElement(2)
Посмотрим на результат:
MONTOLOGY>zw ^NameDictionaryElement(1),^NameDictionaryElement(2)
^NameDictionaryElement(1,"partUri",0)="akp"
^NameDictionaryElement(1,"partUri",0,"UpdateTime")="62086,66625"
^NameDictionaryElement(1,"ru",0)="АКП"
^NameDictionaryElement(1,"ru",0,"UpdateTime")="62086,66625"
^NameDictionaryElement(2,"partUri",0)="meh"
^NameDictionaryElement(2,"partUri",0,"UpdateTime")="62086,66625"
^NameDictionaryElement(2,"ru",0)="МЕХ"
^NameDictionaryElement(2,"ru",0,"UpdateTime")="62086,66625"
MONTOLOGY>
Как видим оба элемента имеют названия на двух языках ru — русский, partUri — используется в урле. Также видим что названия после добавления не изменялись — истории нет (есть только нулевая текущая версия).
Retrieve
Теперь напишем простейшую программу Dictionary и добавим в неё функцию(метод) retrieve — которая будет возвращать название элемента справочника на требуемом языке и требуемой версии (код из живого проекта):
#; --------------------------------------------------------------------------------------------------
#; Имя программы
#; --------------------------------------------------------------------------------------------------
Dictionary
#; --------------------------------------------------------------------------------------------------
#; Получить элемент справочника.
#; --------------------------------------------------------------------------------------------------
retrieve(id,lang="ru",version=0)
q $g(^NameDictionaryElement(id,lang,version),"")
#; --------------------------------------------------------------------------------------------------
q — сокращённое написание команды quit — (аля return)
$g — сокращённое написание команды $get — то есть безопасное обращение к переменной, если по заданным индексам не будет значения — вернётся умолчание заданное после запятой, в нашем случае пустая строка ""
Теперь приведём примеры вызова нашей функции(подпрограммы) синтаксис вызова следующий:
w $$имяПодпрограммы^имяПрограммы(параметры если есть)
w это сокращённое название команды write
$$ означает что функция, возвращающая значение — написана разработчиком (если она системная то знак доллара один).
Пусть вас не смущает символ ^ в вызове программы, ведь он используется и для обращения к глобалам (глобальным переменным). Дело в том что программы, хранятся в таких же глобалах, вроде тех которые я привёл в примере (об этом я расскажу позже). Итак выполним следующие команды:
MONTOLOGY>w $$retrieve^Dictionary(1)
АКП
MONTOLOGY>w $$retrieve^Dictionary(2)
МЕХ
MONTOLOGY>w $$retrieve^Dictionary(1,"partUri")
akp
MONTOLOGY>w $$retrieve^Dictionary(2,"partUri")
meh
MONTOLOGY>
Конечно эти примеры, не далеко ушли от «школьных». В дальнейшем я планирую рассказать как устроены глобалы индексов, как создавать глобалы правил и программы работающие с этими правилами. Справочники будут иметь более сложные структуры. Но имена этих глобалов останутся прежними (будут просто добавлятся новые) — они действительно используются в живом проекте, и метод retrieve тоже. Обо всём этом в следующих статьях.
Спасибо за внимание.
Буду рад вопросам и замечаниям.
Автор: multik