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

Original author: Rob Tweed
  • Translation
  • Tutorial
Роб Твид (Rob Tweed)
Начало см. часть 1.

Глава 2. SQL/реляционные БД против MUMPS



В этой главе будут изложены основные различия между обычными SQL реляционными базами данных и БД на основе MUMPS.

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

Определим структуры данных


Давайте начнём с основ — определим данные. Для примера мы будем использовать простую БД состоящую из 3-х таблиц:

1. Таблица клиентов (CUSTOMER)
2. Таблица заказов (ORDER)
3. Таблица с перечнем вещей составляющих индивидуальный заказ (ITEM)



Имена таблиц указаны жирным, первичные ключи подчёркнуты.

CUSTOMER
custNo Уникальный номер клиента
name Имя клиента
address Адрес клиента
totalOrders Общее число заказов клиента

ORDER
orderNo Номер заказа
custNo Номер клиента (внешний ключ из таблицы CUSTOMER)
orderDate Дата заказа
invoiceDate Дата счёта-фактуры
totalValue Стоимость заказа

ITEM
orderNo Номер заказа (соотвествующий ключу из таблицы ORDER).
itemNo Артикул вещи
price Цена вещи для клиента (учитывая все скидки).

Отношение один-ко-многим показано на диаграмме. Каждый клиент может иметь много заказов и каждый заказ может состоять из многих вещей.

Число заказов конкретного клиента (CUSTOMER.totalOrders) это суммарное число размещённых клиентом заказов в таблице ORDERS, которые идентифицируются по его номеру.

Цена заказа (ORDER.totalValue) это сумма стоимости всех вещей в заказе, каждая конкретная стоимость определяется полем ITEM.price.

Поля CUSTOMER.totalOrders и ORDER.totalValue не вводятся напрямую пользователем — это вычисляемые поля.

Для SQL/реляционных БД эти определения таблиц должны быть загружены в БД (с помощью CREATE TABLE) прежде чем с помощью SQL можно будет добавлять, изменять и получать записи.

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

Тем не менее важно заметить, что реляционная схема может быть прозрачно наложена на MUMPS-хранилище с целью доступа к данным через SQL-инструменты. Реляционная схема может быть добавлена к существующему MUMPS-хранилищу, когда это станет нужным, предоставляя доступ к записям в нормализованной форме (если структуры поддаются нормализации).

Данные три таблицы будут представлены в MUMPS используя следующие глобалы:

Таблица CUSTOMER

^CUSTOMER(custNo)=name|address|totalOrders

Таблица ORDER

^ORDER(orderNo)=custNo|orderDate|invoiceDate|totalValue

Таблица ITEM

^ITEM(orderNo,itemNo)=price

Связь между CUSTOMER и ORDER будет представлена с помощью глобала:

^ORDERX1(custNo,orderNo)=””

Он будет предоставлять номера заказов по номеру клиента.

В MUMPS вы можете использовать любые имена глобалов. Также вы имеете выбор: использовать по одному глобалу для каждой таблицы (как мы и сделали) или один и тот же глобал для нескольких или всех таблиц.

Для примера мы могли бы использовать один глобал для всей нашу структуры:

^OrderDatabase(“customer”,custNo)= name_”~”_address_”~”_totalOrders
^OrderDatabase(“order”,orderNo)= custNo_”~”_orderDate_”~”_invoiceDate_”~”_totalValue
^OrderDatabase(“item”,orderNo,itemNo)=price
^OrderDatabase(“index1”,custNo,orderNo)=””

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

Также мы выбрали использовать символ тильды (~) как разделитель полей в глобалах (можно выбрать любой другой символ).

Добавление записи в БД

Давайте начнём с очень простого примера. Добавим нового клиента в таблицу CUSTOMER.

SQL

INSERT INTO CUSTOMER (CustNo, Name, Address) VALUES (100, ‘Chris Munt’, ‘Oxford’)

MUMPS

Set ^CUSTOMER(100)= “Chris Munt”_"~"_"Oxford"

"_" — это символ для склеивания (конкатенации) строк.

В правой части мы ввели 2 поля, разделённых символом тильды. К слову говоря, в качестве разделяющего можно использовать действительно любой символ, в том числе служебные (non-printable) символы.

Мы могли бы написать:

Set ^CUSTOMER(100)= “Chris Munt”_$c(1)_"Oxford"

Функция $c(1) означает «символ ASCII-значение которого 1».
$c — это сокращённое название функции $char.

И в этом случае для разделения поле использовался бы символ ASCII 1.

Конечно, в реальной ситуации данные, которые подставляются в INSERT запрос (или в команду MUMPS), хранятся в переменных.

SQL

INSERT INTO CUSTOMER (custNo, name, address) VALUES (:custNo, :name, :address)


Прим. переводчика: в ANSI SQL для указания переменных используется предшествующее двоеточие.


MUMPS

Set ^CUSTOMER(custNo)=name_"~"_address

Выборка записей из БД

SQL

SELECT A.name, A.address FROM CUSTOMER A
WHERE A.custNo = :custNo

MUMPS

Set record=$get(^CUSTOMER(custNo))
Set name=$piece(record,"~",1)
Set address=$piece(record,"~",2)

Замечание об использовании функции $get(). Это удобный способ извлекать значения из глобалов. Если запрошенного элемента не существует, то $get() вернёт null ("").

Если бы мы не использовали $get(), то нужно было бы делать так:

Set record=^CUSTOMER(custNo)

Если запрашиваемого элемента глобала не существует, то MUMPS вернёт ошибку периода исполнения (т.е. данные не определены).

Подобно большинству команд и функций в MUMPS вместо $get() можно использовать сокращение $g():

Set record=$g(^CUSTOMER(custNo))

Удаление записи из БД

SQL

DELETE FROM CUSTOMER A WHERE A.custNo = :custNo

MUMPS

kill ^CUSTOMER(custNo)

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

Выборка нескольких записей

SQL

SELECT A.custNo, A.name, A.address FROM CUSTOMER A

MUMPS

s custNo=”” f s custNo=$order(^CUSTOMER(custNo)) Quit:custNo= “” do
. Set record=$get(^CUSTOMER(custNo))
. Set name=$piece(record,"~",1)
. Set address=$piece(record,"~",2)
. Set totalOrders=$piece(record,"~",3)
. ; добавьте свой код для обработки текущей строки

Заметьте, что мы используем синтаксис с точками (dot-syntax). Строки начинающиеся с точек представляют собой подпрограмму, вызываемую командой do (см. окончание первой строки)

Вы можете сделать всё что вам нужно с каждой строкой внутри подпрограммы, как показывает комментарий (последня строка начинающаяся с ";")

Функция $order в MUMPS один из столпов силы и гибкости глобалов. Суть её работы обычно как правило непонятна для тех кто знаком только с SQL и реляционными БД, поэтому подробнее о ней читайте в главе 1.

С функцией $order и глобалами, мы можем обойти любые строки в таблице, ключи к которым начинаются и заканчиваются с любых значений. Важно понимать, что глобалы представляют собой иерахическое хранилище. Ключ в таблице мы эмулировали через индекс в глобале, так что мы не имеем доступа к строкам в последовательности, в которой они создавались: функция $order может быть применена к каждому индексу (ключу) глобала независимо.

Использование MUMPS-функций для высокоуровнего доступа к данным

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

Добавление новой записи в БД

setCustomer(custNo,data) ; Определение заголовка функции
If custNo="" Quit 0
Set ^CUSTOMER(custNo)=data("name")_"~"_data("address")
Quit 1

Эта функция может быть вызвана следующим путём::

kill data ; удалим локальный массив data (он существует только в RAM)
set data("name")="Rob Tweed"
set data("address")="London"
set custNo=101
set ok=$$setCustomer(custNo,.data)  ; $$ означает, что функция пользовательская

Обратите внимание на точку перед параметром функции data. Это вызов по ссылке. data — это локальный массив, не простая переменная, так что мы передаём его в функцию по ссылке.

Функция setCustomer может содержаться в другой программе (например в myFunctions). А поскольку программы в MUMPS содержатся в глобалах, то для вызова функции из глобала нужно написать $$setCustomer^myFunctions(custNo,.data)

Пример:
kill data ; clear down data local array
set data("name")="Rob Tweed"
set data("address")="London"
set custNo=101
set ok=$$setCustomer^myFunctions(custNo,.data)

Функцию $$setCustomer() можно назвать внешней (extrinsic). В терминах ООП это соответствует public. Мы можем обратиться к ней, даже, если она содержится в другой программе. Внешние функции это своего рода методы доступа к данным.

Функция $$setCustomer() возвращает ноль (т.е. false), если в качестве номера клиента мы передадим null. В других случаях запись будет сохранена и $$setCustomer() вернёт 1 (т.е. true). Вы можете проверять переменную ok, чтобы проверить выполнено сохранение или нет.

Поскольку у нас в таблице CUSTOMER есть вычисляемое поле totalOrders, сделаем его поддержку в коде:

setCustomer(custNo,data) ;
if custNo="" Quit 0
; Посчитаем число заказов
Set data(“totalOrders”)=0
Set orderNo=””
for set orderNo=$order(^ORDERX1(custNo,orderNo)) Quit:orderNo=”” do
. set data(“totalOrders”)=data(“totalOrders”)+1
set ^CUSTOMER(custNo)=data("name")_"~"_data("address")_”~”_data(“totalOrders”)
Quit 1

Мы продолжим обсуждение вычисляемых полей позже в секции «Триггеры».

Получение записи из БД

Следующая внешняя функция вернёт строку из таблицы CUSTOMER.

getCustomer(custNo,data) ;
new record
kill data ; clear down data array
if custNo="" Quit 0
set record=$get(^CUSTOMER(custNo))
set data("name")=$piece(record,"~",1)
set data("address")=$piece(record,"~",2)
set data("totalOrders")=$piece(record,"~",3)
Quit 1

Эта функция может быть использована следующим образом:

S custNo=101
set ok=$$getCustomer(custNo,.data)

Она вернёт локалный массив (а точнее говоря изменит его, т.к. он передаётся по ссылке), содержащий 3 поля из строки заданного клиента:

data(“name)
data(“address”)
data(“totalOrders”)

Удаление записи из БД

Следующая внешняя функция удалит строку из таблицы CUSTOMER:

deleteCustomer(custNo) ;
if custNo="" Quit 0
kill ^CUSTOMER(custNo)
Quit 1

Эта функция может быть использована следующим образом:

S custNo=101
S ok=$$deleteCustomer(custNo)


Прим. Переводчика: В 3 части мы поговорим о вторичных индексах, триггерах и транзакциях.

3-я часть, окончание.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 4

    +1
    А скажите какой из подходов лучше с точки зрения производительности и архитектуры:
    1. Set ^CUSTOMER(100)= “Chris Munt”_"~"_«Oxford»
    2.
    Set ^CUSTOMER(100,«name»)= “Chris Munt”
    Set ^CUSTOMER(100,«address»)=«Oxford»

    Мне отвечали как правило, что это вопрос предпочтений.
      0
      В данном случае речь идёт о записи в БД. Поскольку в первом случае нужно сформировать один индексный элемент, а во втором два, то первый будет работать быстрее.

      С точки зрения того, что структуру можно легко менять второй способ предпочтительней, так как в первом мы будем использовать при считывании:
      $piece(record,"~",1)
      $piece(record,"~",2)

      А это приведёт к тому, что при изменении структуры (например мы добавим возраст перед именем или удалим имя) все цифры «поплывут» и MUMPS-программа станет неработоспособна.
        +2
        На самом деле вариантов не два, а сотни — это же MUMPS. Это именно вопрос предпочтений и архитектуры. Ведь вы здесь спрашиваете о том, как лучше записать в базу. А потом вам обязательно потребуется эти данные прочитать — и снова возникнет вопрос скорости и удобства разработки. Кроме того, если система получится долгоживущей, то ее нужно будет поддерживать, а значит менять структуру данных. Поэтому неплохо бы позаботиться о будущем по возможности безболезненном изменении метаданных.
        Что касаемо заданного вопроса — однозначно можно сказать, что в Caché первый вариант реализован эффективнее и универсальнее с помощью списков. В данном случае будет выглядеть как:
        set ^CUSTOMER(100)=$LB(«Chris Munt»,«Oxford»)
        чтобы потом
        name=$LG(^CUSTOMER(100),1)
        address=$LG(^CUSTOMER(100),2)
        $LG(LISTGET) работает намного быстрее $piece, и не нужно «приносить в жертву» данным символ-разделитель — вдруг вы захотите его использовать в данных?

        А если говорить об архитектуре — при разработке любой более-менее серьезной MUMPS системы, разработчики создают собственное API по записи и чтению данных в их придуманную архитектуру данных и потом поддерживают это.

        Собственно это одна из причин появления объектной СУБД в Caché — чтобы разработчикам не придумывать и не поддерживать API для работы с глобалами самим, можно возложить это на InterSystems и использовать объектную архитектуру, поддерживаемую и развиваемую компанией, в которой есть и прямой доступ к глобалам (set, get), но есть и ODBC, JDBC, .NET, java, node.js и т.д.
        Но если вам не нужна развитая, поддерживаемая архитектура, у вас всегда есть глобалы MUMPS и их скорость и свобода.
        Ну и еще раз, отвечая на поставленный вопрос — InterSystems в хранении данных объектов Caché по умолчанию использует именно первый вариант, но на $LB конструкциях.

          0
          Часто выбирают первый вариант.
          Но если у класса будут тысячи, десятки тысяч свойств — наступит предел для одного значения узла. В разных системах он разный — у Caché такой.

          В классах Caché по умолчанию используется первый (но с $LB), но если необходимо, можно поменять структуру хранения на второй вариант или любой какой угодно еще.

        Only users with full accounts can post comments. Log in, please.