Pull to refresh

Глобалы MUMPS: Экстремальное программирование баз данных. Часть 3

Reading time 9 min
Views 8K
Original author: Rob Tweed
Роб Твид (Rob Tweed)
Начало см. часть 1, часть 2.

Вторичные индексы

В реляционных базах данных вторичные индексы задаются как правило при определении таблиц, или после с помощью ALTER TABLE. Если индекс определён, то он автоматически создаётся, а потом поддерживается и пересчитывается базой данных при изменении данных.

В MUMPS индексы обслуживаются явно программистом, например, в функции обновления таблицы.

В следствии иерархической природы MUMPS-хранилища поле с первичным индексом используется как ключ.

Например, рассмотрим MUMPS-функцию по добавлению новой строки в ORDER:
setOrder(orderNo,data) ;
new rec,itemNo,ok
if orderNo="" Quit 0
; Вычислим общую стоимость заказа
set data("totalValue")=0
set itemNo=""
for set itemNo=$Order(^ITEM(orderNo,itemNo)) Quit:itemNo="" do
. set ok=$$getItem(orderNo,itemNo,.itemData)
. Set data("totalValue")=data("totalValue")+itemData("price")
set rec=data("custNo")_"~"_data("orderDate")_"~"_data("invoiceDate")
set rec=rec_"~"_data("totalValue")
set ^ORDER(orderNo)=rec
Quit 1


Обратите внимание на код для вычисления общей стоимости заказа. Мы проходим все записи из ITEM для определённого номера заказа orderNo и используем функцию $$getItem() для получения стоимости каждой вещи. Код функции $$getItem() таков:

getItem(orderNo,itemNo,itemData)
kill itemData
s itemData("price")=0
if orderNo="" Quit 0
if itemNo="" Quit 0
if ‘$data(^ITEM(orderNo,itemNo)) Quit 0
set itemData("price")=^ITEM(orderNo,itemNo)
Quit 1


Посмотрите на сроку, которая проверяет существование элемента в глобале для определённого номера заказа и номера вещи. Там используется MUMPS-функция $data и оператор отрицания одинарная кавычка `.

Давайте добавим индекс для быстрого доступа к покупкам каждого покупателя.

Для хранения индекса создадим новый глобал ^ORDERX1. Будем сохранять в глобале пару ключей: номер покупателя (custNo) и номер заказа (orderNo).

Чтобы индекс (custNo, orderNo) заработал расширим функцию setOrder следующим образом:

setOrder(orderNo,data) ;
new rec,itemNo,ok
if orderNo="" Quit 0
; Посчитаем стоимость заказа
set data("totalValue")=0
set itemNo=""
for set itemNo=$Order(^ITEM(orderNo,itemNo)) Quit:itemNo="" do
. set ok=$$getItem(orderNo,itemNo,.itemData)
. Set data("totalValue")=data("totalValue")+itemData("price")
set rec=data("custNo")_"~"_data("orderDate")_"~"_data("invoiceDate")
set rec=rec_"~"_data("totalValue")
set ^ORDER(orderNo)=rec
if data("custNo")`="" set ^ORDERX1(data("custNo"),orderNo)=""
Quit 1


Для создания заказа будем использовать эту функцию так:
set orderNo=21
kill data
set data("custNo")=101
set data("orderDate")="4/5/2003"
set data("invoiceDate")="4/7/2003"
set ok=$$setOrder(orderNo,.data)


Ссылочная целостность

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

Процесс поддержания ссылочной целостности отвечает за поддержание смысловой (в оригинале — semantic) целостности между связанными таблицами. В особенности, это связано с поддержанием взаимосвязей между таблицами на основе NATURAL JOIN (разные таблицы имеют столбцы которые хранят значения одного и того же типа, по которым и строится взаимосвязь между таблицами) или первичных/внешних ключей.

Например, когда номер клиента CUSTOMER.custNo меняется или удаляется, то в порядке поддержания смысловой целостности между таблицей клиентов и таблицей заказов должно быть произведено соотвествующее изменение поля ORDER.custNo.

Аналогично, когда номер заказа ORDER.orderNo меняется (или удаляется), то в порядке поддержания смысловой целостности между таблицей с заказами и таблицей вещей (из которых состоят заказы), соотвествующее изменение должно быть произведено с полем ITEM.orderNo.

В реляционных БД (RDBMS) правила обеспечения целостности задаются при создании таблиц с помощью первичных и внешних ключей. В MUMPS эти правила могут быть реализованы прямо внутри наших функций.

Помня о взаимосвязи между таблицами клиентов и их заказов, операция обновления CUSTOMER будет такой:

SQL

UPDATE CUSTOMER A
SET custNo = :newCustNo
WHERE A.custNo = :oldCustNo

В результате запроса будут обновлены соответствующие записи в таблице ORDER (по связи CUSTOMER.custNo=ORDER.custNo), если при определении структуры БД были правильно заданы соотвествующие внешние ключи.

MUMPS

updateCustomer(oldCustNo,newCustNo,newData) ;
new result,orderData,orderNo
if (oldCustNo="")!(newCustNo="") Quit 0
set orderNo=""
for set orderNo=$order(^ORDERX1(oldCustNo,orderNo)) Quit:orderNo="" do
. set result=$$getOrder(orderNo,.orderData)
. set orderData("custNo")=newCustNo
. set ok=$$setOrder(orderNo,.orderData)
set ok=$$setCustomer(newCustNo,.newData)
if newCustNo`=oldCustNo set ok=$$deleteCustomer(oldCustNo)
Quit 1


Заметьте, что этот код большей частью состоит из уже созданных нами функций для обслуживания таблиц CUSTOMER и ORDER. В MUMPS просто повторно использовать код.

Теперь создадим функцию $$getOrder:

getOrder(orderNo,orderData) ;
new record
if (orderNo="") Quit 0
set record=$g(^ORDER(orderNo))
set orderData("custNo")=$piece(record,"~",1)
set orderData("orderDate")=$piece(record,"~",2)
set orderData("invoiceDate")=$piece(record,"~",3)
set orderData("totalValue")=$piece(record,"~",4)
Quit 1

Нам также нужно расширить нашу изначальную простую функцию $$deleteCustomer(). Аналогичные соображения применим к операциям удаления строк из таблицы клиентов.

SQL-запрос и его эквивалент на M показаны ниже:

SQL

DELETE FROM CUSTOMER A
WHERE A.custNo = :custNo

В результате этого запроса соотвествующие записи о заказах будут удалены из таблицы ORDER (согласно связи CUSTOMER.custNo=ORDER.custNo и правилам целостности определённым при создании схемы БД). Правила целостности можно задать с помощью внешних ключей, например.

MUMPS

deleteCustomer(custNo) ;
new orderNo
if custNo="" Quit 0
set orderNo=""
for set orderNo=$order(^ORDERX1(custNo,orderNo)) Quit:orderNo="" do
. set result=$$deleteOrder(custNo,orderNo)
kill ^CUSTOMER(custNo)
Quit 1


Для кода выше нужна функция $$deleteOrder(). Вот она:

deleteOrder(custNo,orderNo) ;
kill ^ITEM(orderNo) ; Удалим все вещи входящие в заказ
kill ^ORDER(orderNo) ; Удалим заказ
kill ^ORDERX1(custNo,orderNo) ; Удалим связь между клиентом и номером заказа, используемую как вторичный индекс
Quit 1

Заметьте, что записи о всех вещах входящих в определённый заказ удаляются всего одной командой KILL по индексу с номером заказа. Это происходит потому, что между таблицами с клиентами и номерами их заказов есть каскадная связь (в терминах SQL).

Если при удалении клиента нужно сохранять информацию о его заказах, разрывая связь между клиентом и его заказами, то функция deleteCustomer примет такой вид:

deleteCustomer(custNo) ;
new orderNo,result,orderData
if custNo="" Quit 0
set orderNo=""
for set orderNo=$order(^ORDERX1(custNo,orderNo)) Quit:orderNo="" do
. set result=$$getOrder(orderNo,.orderData)
. set orderData("custNo")=""
. set result=$$setOrder(orderNo,.orderData)
kill ^CUSTOMER(custNo)
Quit 1

Аналогичная логика должна быть применена к данным хранящимся в таблице ITEM, когда изменяется или удаляется номер заказа ORDER.orderNo в таблице ORDER.

Триггеры

Триггеры — это простой способ вызова некого предопределённого кода при выполнении некоторых условий внутри БД. Обычно триггеры срабатывают при изменении некоторой информации в БД.

В реляционных БД триггеры определяются на уровне схемы БД. В MUMPS мы можем разместить любой триггерный код в функциях, которые обслуживают БД.

Возьмём, например, число заказов конкретного клиента CUSTOMER.totalOrders. Мы должны определить триггер для автоматического обновления этого поля при добавлении или удалении заказа из таблицы ORDER.

SQL

Данное SQL-выражение должно быть включено в код триггера для таблицы ORDER с целью обеспечения корректного значения в CUSTOMER.totalOrders:

SELECT COUNT(A.orderNo)
FROM A.ORDER
WHERE A.custNo = :custNo


Этот запрос будет срабатывать на операциях вставки и удаления строк в таблице ORDER (согласно связи CUSTOMER.custNo=ORDER.custNo)

MUMPS

Мы просто добавим следующий (триггерный) код в функцию вставки строки для таблицы ORDER:

setOrder(orderNo,data) ;
new rec,itemNo,ok
if orderNo="" Quit 0
; Подсчитаем общую стоимость вещей в заказе
set data("totalValue")=0
set itemNo=""
for set itemNo=$Order(^ITEM(orderNo,itemNo)) Quit:itemNo="" do
. set ok=$$getItem(orderNo,itemNo,.itemData)
. Set data("totalValue")=data("totalValue")+itemData("price")
set rec=data("custNo")_"~"_data("orderDate")_"~"_data("invoiceDate")
set rec=rec_"~"_data("totalValue")
set ^ORDER(orderNo)=rec
; Сохраним информацию для нашего вторичного индекса
if data("custNo")`="" set ^ORDERX1(data("custNo"),orderNo)=""
;
; Обновим значения поля CUSTOMER.totalOrders
new custData
Set ok=$$getCustomer(data("custNo"),.custData)
; При сохранении клиента будет автоматически пересчитано CUSTOMER.totalOrders. См. определение функции setCustomer в предыдущей части статьи
Set ok=$$setCustomer(data("CustNo"),.custData)
;
Quit 1

Эти же соображения применимы при удалении строк из таблицы ORDER. Подобная схема может использована для автоматического обновления стоимости заказа ORDER.totalValue при добавлении вещи в заказ.

SQL

Следующий SQL-код должен быть размещён внутри триггера для таблицы ITEM для вычисления нового значения стоимости заказа ORDER.Value:

SELECT SUM(A.price)
FROM A.ITEM
WHERE A.orderNo = :orderNo

Запрос будет срабатывать для всех операций вставки и удаления на таблице ITEM (согласно связи ORDER.orderNo=ITEM.orderNo).

MUMPS

Добавим следующий (триггерный) код в функцию вставки строк с таблицу ITEM:

setItem(orderNo,itemNo,data) ;
new ok
if (orderNo="")!(itemNo="") Quit 0
set ^ITEM(orderNo,itemNo)=data("price")
set^ORDERX1(custNo,orderNo)=""

; Обновим значение поля ORDER.totalValue
set ok=$$getOrder(orderNo,.orderData)
; При сохранении заказа поле ORDER.totalValue будет пересчитано
set ok=$$setOrder(orderNo,.orderData)
Quit 1

Те же соображения применимы к операциям по удалению строк из таблицы ITEM.
Следующим примером использования триггеров в нашей базе будет автоматическая генерация счёта-фактуры для клиента, как только дата его создания будет сохранена в поле ORDER.invoiceDate. Мы можем очень просто добавить эту функциональность в нашу процедуру обновления таблицы ORDER:

setOrder(orderNo,data) ;
new rec,itemNo,ok
if orderNo="" Quit 0
; Подсчитаем стоимость заказа
set data("totalValue")=0
set itemNo=""
for set itemNo=$Order(^ITEM(orderNo,itemNo)) Quit:itemNo="" do
. set ok=$$getItem(orderNo,itemNo,.itemData)
. Set data("totalValue")=data("totalValue")+itemData("price")
set rec=data("custNo")_"~"_data("orderDate")_"~"_data("invoiceDate")
set rec=rec_"~"_data("totalValue")
set ^ORDER(orderNo)=rec
if data("custNo")`="" set ^ORDERX1(data("custNo"),orderNo)=""
;
; Обновим поле CUSTOMER.totalOrders
new custData
Set ok=$$getCustomer(data("custNo"),.custData)
Set ok=$$setCustomer(data("CustNo"),.custData)
;
; Генерируем счёт-фактуру, если присутствует его дата
if Data("invoiceDate")`="" Set Result=$$invoiceOrder(orderNo)
;
Quit 1

Конечно, функция $$invoiceOrder() должна быть написана, чтобы произвести все необходимые действия по генерации счёта-фактуры.

Транзакции

Завершённое обновление БД часто состоит из многих обновлений некоторого числа таблиц. Все эти сопутствующие обновления должны быть гарантированно выполнены прежде чем основное обновление (или транзакцию) можно будет считать завершённым.

Как правило в реляционных БД транзакции включены по умолчанию. Другими словами изменения в БД не записываются до тех пор пока модифицирующий процесс не даст команду COMMIT, после чего всё изменения подтверждаются и начинается новая транзакция.

MUMPS очень похож в этом отношении, за исключением того, что транзакции должны быть явно включёны. Программа должна обязательно выполнить команду запуска транзакции TSTART, а не только подтвердить её завершение TCOMMIT.

SQL

Подтверждаем текущую транзакцию и (неявно) запускаем новую:

COMMIT

MUMPS

Начать новую транзакцию:

TSTART

Подтвердить транзакцию:

TCOMMIT

Если управление транзакциями явно не включено в MUMPS-системе, то все обновления будут немедленно применены к текущему хранилищу. Иными словами любая SET или KILL команда на глобале или его элементе может быть рассмотрена как завершённая транзакция.

Выводы

Есть много ключевых преимуществ использования MUMPS по сравнению с традиционными реляционными БД.

  • Повторное использование кода и простота сопровождения. При аккуратном программировании можно достичь экстремального высокого уровня повторного использования кода. Вышеприведённые примеры показывают как можно инкапсулировать все действия с БД в рамках функций set, get и delete для каждой таблицы. Пишите эти функции один раз, а потом используйте их сколько хотите.
  • Переносимость. Определения структур данных содержатся внутри функций, которые работают с ними. Нет никакого различия между определением данных и их реализацией.
  • Гибкость. Код, обновляющий данные, может быть изменён на срабатывание по любому действию или событию внутри системы.
  • Производительность и оптимизация. Опытные MUMPS-аналитики увидят возможности для оптимизации функций использованных нами для работы с БД. Это возможно потому, что определение данных и реализация операций над ними содержатся вместе внутри конкретных функций. Аналитик использующий SQL на имеет точного контроля над тем, каким образом обрабатываются триггеры, действия по обеспечению ссылочной целостности и транзакции.
  • Преимущества использования SQL для работы или извлечения данных (для этого можно использовать специальные утилиты) не потеряны, потому что слой определения данных может быть прозрачно наложен на существующие MUMPS-таблицы (глобалы). Это можно сделать в последствии. Например, Caché SQL и сторонняя утилита KB_SQL реализуют полноценное SQL-окружение поверх глобалов MUMPS.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+11
Comments 46
Comments Comments 46

Articles