Статьи

6 советов по отладке динамического кода SQL

Получил эту просьбу о помощи через нашу программу  AskTOM PL / SQL Office Hours  :

Dear Experts, I have below written below code:
----------------------------------------------
       Declare
       v_Table all_tables.TABLE_NAME%type;
       v_Mnt varchar2(2):='08';
       Type Cur_type is Ref Cursor;
        C Cur_type;

       Begin
               v_Table:='ddi_ticket_10_1018';           
               Open C for 'SELECT * from bill.'||v_Table||v_Mnt||'Where called_nbr=123';

               End;
-------------------------------------------------------------------
When executing this code, I face this Error message.
ORA-00933-SQL Command not properly ended
ORA-06512: At Line 9.
Please check the above code and modify for syntax correction

Я мог бы, на первый взгляд, довольно хорошо догадаться, в чем проблема.

Ты можешь?

Я не пытаюсь похвастаться. Я просто призываю вас  не  читать дальше, а вместо этого изучить код. Что может быть причиной его проблемы?

Динамический SQL может быть сложным — не так много, прежде чем OPEN-FOR или EXECUTE IMMEDIATE являются сложными частями языка PL / SQL. Но потому что чертовски легко испортить SQL или PL / SQL, который вы создаете динамически. Ты мог:

  • Оставьте «;» (из кода PL / SQL).
  • Забудьте оставлять пробелы между разделами вашего SQL.
  • Есть непревзойденные скобки.
  • И так далее.

В этом случае я ответил: «Я почти уверен, что вы обнаружите проблему в том, что у вас нет пробела перед ключевым словом« Где »в:

v_Mnt||'Where called_nbr=123';

Затем этот обмен напомнил мне, что мне следует написать статью с несколькими простыми советами, которые позволят упростить отладку динамического кода SQL и обеспечить его работу по назначению. Поехали.

  1. Определите вашу подпрограмму с AUTHID CURRENT_USER ( вызывает права ).
  2. Если вы выполняете динамический  DDL , сделайте подпрограмму  автономной транзакцией .
  3. Всегда ВЫПОЛНИТЬ НЕМЕДЛЕННЫЙ или ОТКРЫТЬ ДЛЯ переменной.
  4. Всегда обрабатывайте исключение, которое может возникнуть при выполнении динамического SQL.
  5. Зарегистрируйте информацию об ошибке  плюс  переменную, которую вы пытались выполнить.
  6. Создайте тестовый режим в вашей подпрограмме.

Я продемонстрирую ценность этих пунктов, начав с версии супер-пупер полезной + опасной программы, которая игнорирует все из них: процедура drop_whwhat.

PROCEDURE drop_whatever (nm  IN VARCHAR2 DEFAULT '%',
                         typ IN VARCHAR2 DEFAULT '%')
IS
   CURSOR object_cur
   IS
      SELECT object_name, object_type
        FROM user_objects
       WHERE     object_name LIKE UPPER (nm)
             AND object_type LIKE UPPER (typ)
             AND object_name <> 'DROP_WHATEVER';
BEGIN
   FOR rec IN object_cur
   LOOP
      EXECUTE IMMEDIATE
            'DROP '
         || rec.object_type
         || ' '
         || rec.object_name
         || CASE
               WHEN rec.object_type IN ('TABLE', 'OBJECT')
               THEN
                  ' CASCADE CONSTRAINTS'
               ELSE
                  NULL
            END;
   END LOOP;
END;

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

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

EXEC drop_whatever()

И это опасно  по той же причине .

Ох, но подожди. Учитывая, насколько это полезно, возможно, мы должны позволить  каждому  быть в состоянии использовать это. Я знаю, я буду запускать эту команду:

GRANT EXECUTE ON drop_whatever TO PUBLIC

Эй, что может пойти не так? 🙂

Так очень, очень. Давайте пройдемся по моим рекомендациям и выделим потенциальные проблемы.

1. Определите свою подпрограмму с AUTHID CURRENT_USER ( вызывает права ).

В процедуре нет предложения AUTHID (держу пари, что большинство ваших сохраненных программных блоков не имеют). Это означает, что по умолчанию используется «определитель прав». Это означает, что он всегда выполняется с привилегиями определителя / владельца процедуры.

Это означает, что если, скажем, HR владеет drop_whwhat, а затем SCOTT выполняет его (спасибо, GRANT PUBLIC!) Как в:

EXEC HR.drop_whatever()

Тогда SCOTT просто отбросит все объекты базы данных  в схеме HR !

2. Если вы выполняете динамический  DDL , сделайте подпрограмму  автономной транзакцией .

Особенность операторов DDL заключается в том, что Oracle выполняет  неявную фиксацию  как до, так и после выполнения оператора DDL. Поэтому, если у вас есть хранимая процедура, которая выполняет динамический DDL, вы должны либо предупредить всех, кто может ее использовать, о любых ожидающих изменениях в их сеансе (это просто грубо), либо добавить этот оператор в вашу процедуру:

PRAGMA AUTONOMOUS_TRANSACTION;

Теперь любые коммиты (или откаты), выполненные в процедуре, будут влиять только на те изменения, которые были внесены  в  процедуру.

3. Всегда ВЫПОЛНЯЙТЕ НЕМЕДЛЕННЫЙ или ОТКРЫТЫЙ ДЛЯ ПЕРЕМЕННОЙ.

Это такая простая вещь, но она может сэкономить вам много времени при попытке выяснить, что не так с вашей программой.

Вот в чем дело: не сложно понять, как использовать EXECUTE IMMEDIATE. Но это может быть  очень  сложно правильно построить вашу строку во время выполнения. Так много мелких ошибок может вызвать ошибки. И если вы создаете свою строку непосредственно в операторе EXECUTE IMMEDIATE, как вы можете увидеть, что было выполнено и где вы могли ошибиться?

Предположим, например, что в процедуре drop_whwhat я построил свой оператор DROP следующим образом:

EXECUTE IMMEDIATE
   'DROP '
 || rec.object_type
 || rec.object_name ...

Когда я пытаюсь бросить свой стол, я вижу:

ORA-00950: invalid DROP option

И что это говорит мне? Немного. Какой вариант, по его мнению, я дал, что это недействительно? Что я только что попытался сделать?

Если, с другой стороны, я назначаю выражение, которое я хочу выполнить, переменной, а затем вызываю EXECUTE IMMEDIATE, я могу перехватить ошибку и записать / отобразить эту переменную (см. Вторую реализацию drop_whither ниже). И тогда я мог бы увидеть что-то вроде:

DROP SYNONYMV$SQL - FAILURE

Ой! Я вижу сейчас. Я не включил пробел между типом объекта и именем объекта. Я такой глупый. Поэтому всегда объявляйте переменную, присваивайте динамически построенный оператор SQL этой переменной и выполняйте ее немедленно.

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

5. Зарегистрируйте информацию об ошибке  плюс  переменную, которую вы пытались выполнить.

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

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

6. Постройте тестовый режим в своей подпрограмме.

Я долго писал код и портил этот код так долго, я понял, что очень полезно — особенно когда этот код вносит изменения в данные в таблицах — реализовать тестовый режим, который ничего не «делает». Просто показывает мне, что бы он сделал, если бы я позволил.

Вы можете видеть это в коде ниже, когда я передаю TRUE (по умолчанию) для параметра just_checking.

Намного лучше (?)

«?» в этом названии просто чтобы напомнить нам, что эта процедура по своей природе опасна.

Вот версия drop_wh независимо от моих рекомендаций. Обратите внимание, что для реального производственного кода вы никогда не должны   «сообщать» или «регистрировать» ошибку, вызывая DBMS_OUTPUT.PUT_LINE. Кто увидит это? Вместо этого вызовите свою стандартную процедуру регистрации ошибок, и если у вас ее нет, то получите и используйте  Logger .

PROCEDURE drop_whatever (
   nm              IN   VARCHAR2 DEFAULT '%'
 , typ             IN   VARCHAR2 DEFAULT '%'
 , just_checking   IN   BOOLEAN DEFAULT TRUE
)
AUTHID CURRENT_USER
IS
   PRAGMA AUTONOMOUS_TRANSACTION;                             
   dropstr   VARCHAR2 (32767);

   CURSOR object_cur
   IS
      SELECT object_name, object_type
        FROM user_objects
       WHERE object_name LIKE UPPER (nm)
         AND object_type LIKE UPPER (typ)
         AND object_name <> 'DROP_WHATEVER';         
BEGIN
   FOR rec IN object_cur
   LOOP
      dropstr :=
            'DROP '
         || rec.object_type
         || ' '
         || rec.object_name
         || CASE
               WHEN rec.object_type IN ('TABLE', 'OBJECT')
                  THEN ' CASCADE CONSTRAINTS'
               ELSE NULL
            END;                                              

      BEGIN
         IF just_checking
         THEN
            DBMS_OUTPUT.put_line (dropstr || ' - just checking!');
         ELSE
            EXECUTE IMMEDIATE dropstr;
            DBMS_OUTPUT.put_line (dropstr || ' - SUCCESSFUL!');
         END IF;

      EXCEPTION
         WHEN OTHERS
         THEN
            DBMS_OUTPUT.put_line (dropstr || ' - FAILURE!');
            DBMS_OUTPUT.put_line (DBMS_UTILITY.format_error_stack);
      END;
   END LOOP;
END;

Давайте подведем итоги

Когда вы пишете хранимую программную единицу, которая содержит динамический SQL:

  1. Определите вашу подпрограмму с AUTHID CURRENT_USER ( вызывает права ).
  2. Если вы выполняете динамический  DDL , сделайте подпрограмму  автономной транзакцией .
  3. Всегда ВЫПОЛНИТЬ НЕМЕДЛЕННЫЙ или ОТКРЫТЬ ДЛЯ переменной.
  4. Всегда обрабатывайте исключение, которое может возникнуть при выполнении динамического SQL.
  5. Зарегистрируйте информацию об ошибке  плюс  переменную, которую вы пытались выполнить.
  6. Создайте тестовый режим в вашей подпрограмме.