Статьи

NuoDB 1.2 Хранимые процедуры SQL

Первоначально автор Альберто Массари

Вступление

Одной из новых функций предварительного просмотра СУБД NewSQL NuoDB 1.2 является добавление хранимых процедур SQL к набору инструментов, который есть в распоряжении пользователей. Хотя они не добавляют никаких новых возможностей в ядро ​​базы данных, ту же задачу можно выполнить с помощью клиентского кода, написанного на любом из поддерживаемых языков (C ++, Java, .NET, Python, Perl …), значительная часть Сообщество SQL уже привыкло писать их в основном для повышения производительности и безопасности.

Преимущество в производительности можно увидеть, когда процедура выполняет несколько операторов SQL: на самом деле, при запуске внутри механизма транзакций, обходные пути в сети, необходимые для отправки оператора и извлечения данных, удаляются. С другой стороны, это перемещает нагрузку на процессор, связанную с инструкциями потока управления, с клиента на сервер базы данных, хотя эта дополнительная нагрузка должна быть незначительной. Аспект безопасности часто связан с тем, как устроена база данных: может существовать набор таблиц, реализующих модель данных, которыми администратор БД не желает свободно манипулировать, и чья привилегия UPDATE предоставляется ограниченному числу пользователей. В таких случаях,единственной точкой входа для манипулирования этими таблицами является хранимая процедура, которая может проверять данные и затем выполнять несколько команд INSERT, которые гарантируют согласованность модели данных. Итак, если вы один из пользователей, которые чувствуют необходимость в хранимых процедурах, вы можете создать их в NuoDB следующим образом:

CREATE PROCEDURE myProc(IN p1 INTEGER, 
                        INOUT p2 VARCHAR(256), 
                        OUT p3 DECIMAL(18,2))
RETURNS rows(col1 INTEGER, col2 VARCHAR(256))
LANGUAGE SQL
SECURITY INVOKER
AS
....
END_PROCEDURE

Язык по умолчанию (и единственный), на котором могут быть написаны хранимые процедуры, — это SQL, поэтому модификатор LANGUAGE SQL на самом деле не нужен; но мы работаем над внедрением других языков высокого уровня, чтобы вы могли превратить существующий клиентский код в хранимые процедуры. Следите за новостями! В любом случае не каждый может создать хранимую процедуру: только пользователи, которым администратор предоставил роль администратора базы данных, могут создавать их. Вновь созданная процедура может быть выполнена, изменена или удалена только ее создателем; она должна явно предоставить привилегию EXECUTE и / или ALTER другим пользователям, прежде чем они смогут использовать или изменить ее.

На этом этапе, кто бы ни получил привилегию выполнения в процедуре, сможет получить доступ и управлять объектами, на которые ссылается процедура. То есть, если процедура не создана с модификатором SECURITY INVOKER; в этом случае во время выполнения выполняется проверка, чтобы гарантировать, что текущий пользователь имеет необходимые привилегии. Объявление процедуры определяет контракт между вызывающей стороной и кодом SQL, расположенным между AS и маркером END_PROCEDURE. Вызывающая сторона предоставляет значение для каждого из параметров, помеченных как IN или INOUT, в то время как процедура возвращает вызывающей стороне значения через параметры, объявленные как INOUT или OUT, плюс таблицу в памяти, структура которой определяется модификатором RETURNS. Оба параметра и таблица возврата являются необязательными, поэтому можно объявить процедуру, которая неt принимает параметры и не возвращает никакого значения. Взяв эту процедуру в качестве примера, клиент Java может вызвать ее, используя следующий фрагмент кода:

// prepare the EXECUTE statement
CallableStatement cStmt = 
     connection.prepareCall("EXECUTE myProc(?, ?, ?)");
// set a value for the IN parameter
cStmt.setInt(1, 10);
// set a value for the INOUT parameter
cStmt.setInt(2, 10);
// register the INOUT parameter to retrieve its changed value
cStmt.registerOutParameter(2, java.sql.Types.VARCHAR);
// register the OUT parameter to retrieve its changed value
cStmt.registerOutParameter(3, java.sql.Types.DECIMAL);

ResultSet resultSet = cStmt.executeQuery();
while(resultSet.next()) {
    int col1 = resultSet.getInt("col1");
    String col2 = resultSet.getString("col2");
}

// beyond iterating over the returned result set, 
// get the changed values
String p2 = cStmt.getString(2);
int p3 = cStmt.getInt(3);

Пример AC # может выглядеть следующим образом:

NuoDbCommand cmd = new NuoDbCommand("myProc", connection);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Prepare();
cmd.Parameters["p1"].Value = 10;
cmd.Parameters["p2"].Value = 10;
using (DbDataReader reader = cmd.ExecuteReader())
{
    while (reader.Read())
    {
        int col1 = reader["col1"];
        string col2 = reader["col2"]);
    }
}
string p2 = cmd.Parameters["p2"].Value;
int p3 = cmd.Parameters["p3"].Value;

переменные

Изучив интерфейс процедуры, давайте сосредоточимся на том, чем процедура может манипулировать: хотя обычные операторы, заданные удаленным клиентом, могут работать только с полями ссылочных таблиц, хранимые процедуры также могут использовать переменные, ссылаясь на них в любом месте. где буквальная константа может быть использована. Набор предопределенных переменных — это параметры, объявленные в интерфейсе процедуры, но другие можно объявить в теле процедуры просто с помощью инструкции VAR:

VAR v1;
VAR v2 INTEGER;
VAR v3 = 'initial value';
VAR v4, v5, v6 INT = 0, v7;

Если в объявлении переменной не указан тип данных, при присвоении ему значения проверка не производится.

Манипуляция данными

Большая часть операторов SQL — это те же инструкции DML UPDATE, INSERT, DELETE и EXECUTE / CALL, которые может указывать удаленный клиент. Что касается оператора SELECT, наш выбор состоял в том, чтобы разрешить его в двух разных формах: в качестве правой части оператора присваивания или в качестве курсора цикла FOR.

VAR v1, v2, v3;
v1, v2 = SELECT col1, col2 FROM user.table WHERE col3 = p1;
...
FOR SELECT col1 FROM user.table;
    v3 = col1;
END_FOR;

В операторе присваивания переменные, перечисленные в левой части знака равенства, будут присваиваться каждому столбцу, возвращаемому оператором SELECT с другой стороны; v1 получит значение col1, а v2 получит col2. Если SELECT не идентифицирует какую-либо строку, обе переменные будут содержать значение NULL. Если, с другой стороны, SELECT идентифицирует более одной строки, произойдет ошибка времени выполнения. В этом сценарии можно использовать цикл FOR; он будет извлекать строки одну за другой и для каждой строки будет выполнять перечисленные операторы до команды END_FOR. Если эти операторы ссылаются на одно из полей, перечисленных в операторе SELECT, они будут иметь доступ к соответствующему столбцу текущей строки. Операторы процедуры также имеют доступ к новой таблице, которая не поддерживается никаким хранилищем: it ‘s таблица, объявленная в модификаторе RETURNS, которая будет возвращена вызывающей стороне в конце процедуры. Эта таблица может быть заполнена стандартной командой INSERT.

INSERT INTO rows SELECT col1, col2 FROM user.table WHERE col3 = p1;
INSERT INTO rows VALUES (v1, v2);

Завершая описание цикла FOR, его также можно использовать для перебора курсора, возвращаемого двумя другими инструкциями: EXECUTE и INSERT. В случае FOR EXECUTE цикл будет перебирать строки таблицы, которые вызванная процедура объявляет в модификаторе RETURNS; в случае FOR INSERT цикл будет перебирать таблицу в памяти, которая содержит значения идентификаторов, сгенерированных командой INSERT.

FOR INSERT INTO user.table VALUES (v1);
    v3 = id;
END_FOR;

Управление потоком

Команда IF может использоваться для управления потоком команд; условие теста может быть любым допустимым логическим тестом:

IF (v1 > v2 + 4)
...
ELSE
...
END_IF;

IF (CHARACTER_LENGTH(p2) < v1)
...
END_IF;

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

VAR index INTEGER = 0;
WHILE (index < 100)
...
    index = index + 1;
END_WHILE;

Ранний выход

Операторы внутри цикла WHILE или FOR могут включать оператор BREAK, чтобы сразу перейти к первой инструкции после окончания ближайшего цикла, не ожидая естественного условия выхода. Вместо этого команда RETURN может быть помещена в любую позицию внутри тела процедуры, и она немедленно завершит процедуру с кодом успеха. Команда THROW вместо этого вызовет сбой процедуры, откатив любую операцию DML, которую процедура ранее вызывала, и сообщив вызывающей стороне сообщение об ошибке, указанное в качестве аргумента команды THROW.

IF (p1 < 0)
    THROW 'index out of bounds';
END_IF;

IF (p1 = 0)
    RETURN;
END_IF;

VAR index INTEGER = 0, maxLoops INTEGER = 100;
WHILE (index < p1)
    IF (index > maxLoops)
        BREAK;
    END_IF;
    index = index +1;
END_WHILE;

Обработка исключений

Если инструкция внутри тела процедуры не выполняется (например, INSERT, который вставляет дублирующее значение в первичный ключ, или присвоение, которое указывает значение вне допустимого диапазона целевого типа данных), вся процедура завершается неудачно и выполняет откат текущей транзакции. , Если автор ошибки считает такую ​​ошибку исправимой, он может заключить инструкцию в блок TRY / CATCH и выполнить необходимые операции восстановления. Если, проверяя строку error_message, сбой не соответствует ожидаемому, его можно перебросить с помощью инструкции THROW.

/* example code, don't try this at home! */
VAR tmpID INT = 1;
WHILE (TRUE)
    TRY
        INSERT INTO user.table (ID, COL1) VALUES (tmpID, v1);
        BREAK;
    CATCH(error_message)
        IF (error_message CONTAINING 'duplicate')
            tmpID = tmpID + 1;
        ELSE
            THROW error_message;
        END_IF;
    END_TRY;
END_WHILE;

Выводы

Чтобы протестировать новую функцию, мы изменили код теста DBT-2 (доступный по адресу https://github.com/nuodb/dbt2 ), добавив также версию, в которой используются хранимые процедуры. Вот пример того, как выглядит процедура new_order:

CREATE PROCEDURE new_order(tmp_w_id INT, tmp_d_id INT, tmp_c_id INT,
                           tmp_o_all_local INT, tmp_o_ol_cnt INT,
                           ol_i_id1 INT, ol_supply_w_id1 INT, ol_quantity1 INT,
                           ol_i_id2 INT, ol_supply_w_id2 INT, ol_quantity2 INT,
                           ol_i_id3 INT, ol_supply_w_id3 INT, ol_quantity3 INT,
                           ol_i_id4 INT, ol_supply_w_id4 INT, ol_quantity4 INT,
                           ol_i_id5 INT, ol_supply_w_id5 INT, ol_quantity5 INT,
                           ol_i_id6 INT, ol_supply_w_id6 INT, ol_quantity6 INT,
                           ol_i_id7 INT, ol_supply_w_id7 INT, ol_quantity7 INT,
                           ol_i_id8 INT, ol_supply_w_id8 INT, ol_quantity8 INT,
                           ol_i_id9 INT, ol_supply_w_id9 INT, ol_quantity9 INT,
                           ol_i_id10 INT, ol_supply_w_id10 INT, ol_quantity10 INT,
                           ol_i_id11 INT, ol_supply_w_id11 INT, ol_quantity11 INT,
                           ol_i_id12 INT, ol_supply_w_id12 INT, ol_quantity12 INT,
                           ol_i_id13 INT, ol_supply_w_id13 INT, ol_quantity13 INT,
                           ol_i_id14 INT, ol_supply_w_id14 INT, ol_quantity14 INT,
                           ol_i_id15 INT, ol_supply_w_id15 INT, ol_quantity15 INT)
                              
AS

  VAR out_c_credit, out_c_last, out_d_next_o_id;
  VAR out_w_tax, out_d_tax, out_c_discount;
  VAR tmp_i_name, tmp_i_data, tmp_i_id, tmp_i_price;
  VAR tmp_ol_supply_w_id, tmp_ol_quantity, tmp_ol_amount;
  VAR tmp_s_quantity, tmp_total_amount;
  VAR o_id = 0;

  out_w_tax = SELECT w_tax
              FROM warehouse
              WHERE w_id = tmp_w_id;

  out_d_tax, out_d_next_o_id = SELECT d_tax, d_next_o_id
                               FROM district
                               WHERE d_w_id = tmp_w_id
                                 AND d_id = tmp_d_id;

  o_id=out_d_next_o_id;

  UPDATE district
  SET d_next_o_id = d_next_o_id + 1
  WHERE d_w_id = tmp_w_id
    AND d_id = tmp_d_id;

  out_c_discount, out_c_last, out_c_credit = SELECT c_discount, c_last, c_credit
                                             FROM customer
                                             WHERE c_w_id = tmp_w_id
                                               AND c_d_id = tmp_d_id
                                               AND c_id = tmp_c_id;

  INSERT INTO new_order (no_o_id, no_d_id, no_w_id)
  VALUES (out_d_next_o_id, tmp_d_id, tmp_w_id);

  INSERT INTO orders (o_id, o_d_id, o_w_id, o_c_id, o_entry_d, o_carrier_id, 
                      o_ol_cnt, o_all_local)
  VALUES (out_d_next_o_id, tmp_d_id, tmp_w_id, tmp_c_id, 
          now(), NULL, tmp_o_ol_cnt, tmp_o_all_local);

  tmp_total_amount = 0;

  VAR counter = 0;
  WHILE (counter < 15)
    IF (tmp_o_ol_cnt > counter )

      tmp_i_id = CASE counter
                    WHEN 0 THEN ol_i_id1
                    WHEN 1 THEN ol_i_id2
                    WHEN 2 THEN ol_i_id3
                    WHEN 3 THEN ol_i_id4
                    WHEN 4 THEN ol_i_id5
                    WHEN 5 THEN ol_i_id6
                    WHEN 6 THEN ol_i_id7
                    WHEN 7 THEN ol_i_id8
                    WHEN 8 THEN ol_i_id9
                    WHEN 9 THEN ol_i_id10
                    WHEN 10 THEN ol_i_id11
                    WHEN 11 THEN ol_i_id12
                    WHEN 12 THEN ol_i_id13
                    WHEN 13 THEN ol_i_id14
                    WHEN 14 THEN ol_i_id15
                 END;
      tmp_ol_supply_w_id = CASE counter
                    WHEN 0 THEN ol_supply_w_id1
                    WHEN 1 THEN ol_supply_w_id2
                    WHEN 2 THEN ol_supply_w_id3
                    WHEN 3 THEN ol_supply_w_id4
                    WHEN 4 THEN ol_supply_w_id5
                    WHEN 5 THEN ol_supply_w_id6
                    WHEN 6 THEN ol_supply_w_id7
                    WHEN 7 THEN ol_supply_w_id8
                    WHEN 8 THEN ol_supply_w_id9
                    WHEN 9 THEN ol_supply_w_id10
                    WHEN 10 THEN ol_supply_w_id11
                    WHEN 11 THEN ol_supply_w_id12
                    WHEN 12 THEN ol_supply_w_id13
                    WHEN 13 THEN ol_supply_w_id14
                    WHEN 14 THEN ol_supply_w_id15
                 END;
      tmp_ol_quantity = CASE counter
                    WHEN 0 THEN ol_quantity1
                    WHEN 1 THEN ol_quantity2
                    WHEN 2 THEN ol_quantity3
                    WHEN 3 THEN ol_quantity4
                    WHEN 4 THEN ol_quantity5
                    WHEN 5 THEN ol_quantity6
                    WHEN 6 THEN ol_quantity7
                    WHEN 7 THEN ol_quantity8
                    WHEN 8 THEN ol_quantity9
                    WHEN 9 THEN ol_quantity10
                    WHEN 10 THEN ol_quantity11
                    WHEN 11 THEN ol_quantity12
                    WHEN 12 THEN ol_quantity13
                    WHEN 13 THEN ol_quantity14
                    WHEN 14 THEN ol_quantity15
                 END;

      tmp_i_price, tmp_i_name, tmp_i_data = SELECT i_price, i_name, i_data
                                            FROM item
                                            WHERE i_id = tmp_i_id;

      IF (tmp_i_price > 0 )
          tmp_ol_amount = tmp_i_price * tmp_ol_quantity;

          call new_order_2(tmp_w_id, tmp_d_id, tmp_i_id, tmp_ol_quantity, tmp_i_price,
                           tmp_i_name, tmp_i_data, out_d_next_o_id, tmp_ol_amount,
                           tmp_ol_supply_w_id, counter + 1, tmp_s_quantity);

          tmp_total_amount = tmp_total_amount + tmp_ol_amount;
      END_IF;
    ELSE
      BREAK;
    END_IF;
    counter = counter + 1;
  END_WHILE;

END_PROCEDURE