
Февраль 2005
Профессионалу разработчику
Стивен Фернстайн
Управление мифическим кодом
(Controlling Mythological Cod, by Steven Feuerstein )
Источник: журнал Oracle Magazine, no.5, 2004, http://www.oracle.com/technology/oramag/oracle/04-sep/o54plsql.html
Отыщите и удалите мифы из своего кода.
- “Не используйте такую-то возможность; с ней была проблема в версии n.n.”
- “Всегда используйте явные курсоры; это наиболее эффективный способ выбрать одну строку данных.”
- “Избегайте пакетов; они используют слишком много памяти”.
Думаю, вы слышали в том или ином варианте подобные утверждения. Это лишь некоторые из тех мифов, которые навсегда сохраняются в наших PL/SQL-приложениях. В некоторых случаях, действительно, была серьезная причина для их первоначального появления в наших программах. Во многих других случаях наиболее активной составляющей является невежество. Вне зависимости от первоначальной причины программное обеспечение, которое отражает эти мифы, бывает очень трудно выявить и еще труднее заменить код на “правильный”. Эта статья опровергает ряд наиболее вредных мифов о PL/SQL, показывает, как систематически удалить их, и предлагает советы, как сегодня избежать создания такого кода, который завтра станет мифическим.
"Не используйте такую-то возможность"
Как мифы вкрадываются в наш код? Давайте посмотрим на общепринятый сценарий разработки программного обеспечения: создание кода, обходящего ошибку.
Предположим, что я создаю приложение, основанное на пакете анализа, который поставляется сторонней фирмой, и называется analyze_rates. В пакете есть функция optimal_plan, которая возвращает информацию об оптимальном плане для заданной компании.
CREATE OR REPLACE PACKAGE analyze_rates
IS
TYPE optimal_info_rt IS RECORD (
rate_level PLS_INTEGER,
rate_type PLS_INTEGER,
is_optimal BOOLEAN);
FUNCTION optimal_plan (id_in
IN company.id%TYPE)
RETURN optimal_info_rt;
Если план не оптимальный, функция должна возвращать значение FALSE в поле is_optimal записи optimal_info. К сожалению, эта функция всегда возвращает TRUE в is_optimal, даже если план не является оптимальным. Даже хуже, я не могу изменить этот пакет, поскольку он поставляется сторонней фирмой и “свернут” (wrapped).
К счастью, есть обходной путь: оказывается, что если поля rate level и type имеют значение NULL, то план не является оптимальным.
В большой спешке, я рассылаю памятку своей команде из 25 человек, в которой объясняется проблема и ее решение. Я даю им всем инструкцию: всякий раз, когда они вызывают analyze_rates.optimal_plan, писать код следующим образом.
DECLARE
l_company_id company.id%TYPE;
opt_info analyze_rates.optimal_info_rt;
BEGIN
get_company (l_company_id);
opt_info := analyze_rates.optimal_plan (l_company_id);
IF opt_info.rate_level
IS NULL
AND opt_info.rate_type
IS NULL
THEN
-- Не оптимальный план!
Это работает – сейчас! Итак, все мои разработчики будут копировать и вставлять этот оператор IF в свои программы, везде, где это потребуется. Таким образом, основание мифа заложено.
Что произойдет, когда и если поставщик изменит поведение функции optimal_plan, сделав обходной путь ненужным? Что произойдет, когда поставщик действительно исправит ошибку и optimal_plan будет, наконец, возвращать FALSE в поле is_optimal записи?
Очень маловероятно, что моя команда будет рыскать по своим программам, чтобы найти и заменить сложный оператор IF.
Скорее всего, этот обходной путь станет постоянной частью приложения, то есть, мифом, отражающим старую реальность, вызывающим недоумение, и существенно увеличивающим риск внесения ошибок в будущем.
В конце этой статьи в разделе “Как не пустить мифы в свой код” я покажу, как избегать подобных сценариев и облегчить изменение вашего кода, когда ошибка исправлена и функциональность улучшена, таким образом, уничтожив миф прежде, чем он внедрился в код. Сначала, однако, давайте рассмотрим некоторые из наиболее распространенных мифов в PL/SQL-программах.
"Всегда используйте явные курсоры"
PL/SQL был спроектирован – и показал себя таковым – как самый быстрый и простой способ программирования в базе данных Oracle. Это особенно заметно, когда приходиться выбирать данные из базы данных, поскольку PL/SQL предоставляет различные способы написания и выполнения курсоров SELECT, включая неявные статические курсоры, явные статические курсоры, DBMS_SQL динамические курсоры, собственные (native) динамические SQL курсоры и BULK COLLECT INTO. Явные и неявные статические курсоры являются наиболее часто используемыми способами выбора информации из базы данных.
- Явный курсор:
Просто напишите оператор SELECT в своей программе и включите предложение INTO, чтобы получить запрашиваемые значения, и Oracle сделает всю остальную работу (откроет, выберет, закроет) за вас. Вот пример:
CREATE OR REPLACE PROCEDURE change_team (
id_in IN team_member.id%TYPE
,new_team_id_in IN team_member.team_id%TYPE
)
IS
l_team_member team_member%ROWTYPE;
BEGIN
SELECT * INTO l_team_member
FROM team_member
WHERE id = id_in;
Неявный курсор: Если вы предпочитаете контролировать обработку курсора – и, возможно, хотите повторно использовать ваш SQL-оператор – вы можете объявить свой собственный курсор явно и затем выполнить каждую операцию сами. Вот переписанный вариант той же самой программы change_team, на этот раз с явным курсором:
CREATE OR REPLACE PROCEDURE change_team (
id_in IN team_member.id%TYPE
,new_team_id_in
IN team_member.team_id%TYPE
)
IS
CURSOR member_cur IS
SELECT *
FROM team_member
WHERE id = id_in;
l_team_member member_cur%ROWTYPE;
BEGIN
OPEN member_cur;
FETCH member_cur INTO l_team_member;
Годами многие эксперты в мире Oracle (включая и меня) проповедовали четкую непоколебимую догму о явных и неявных курсорах. Догма звучит слежующим образом:
Всегда нужно использовать явные курсоры (CURSOR <cursor_name> IS) и безусловно избегать неявных курсоров (SELECT INTO), потому что явные курсоры более эффективны. Они являются более эффективными потому, что поведение неявных курсоров должно соответствовать стандарту ANSI, который гласит, что даже для запроса, выбирающего одну строку, вы всегда должны выполнять две выборки: одну, чтобы выбрать строку, и вторую, чтобы проверить, есть ли еще строки (две или более). Следовательно, использование неявных курсоров медленнее, чем использование явных, для которых можно выполнить одну команду FETCH.
Это звучит логично и убедительно, не так ли? Проблема заключается в том, что это было правдой давно в Oracle6, но, безусловно, не является правдой в Oracle8i, Oracle9i, или Oracle 10g. В действительности, неявные курсоры часто работают быстрее, чем явные. То, что явные курсоры являются наиболее эффективными – это один из наиболее живучих PL/SQL-мифов, поскольку он распространялся давно и широко.
Итак, что вы будете делать, когда придется писать курсоры? Я предлагаю следующие рекомендации:
- Где только возможно используйте BULK COLLECT— он дает поразительное увеличение производительности. В Oracle9i Release 2 вы можете даже использовать BULK COLLECT, чтобы выбрать строки непосредственно в коллекцию записей.
- Для выбора только одной строки использование BULK COLLECT не имеет смысла. Какой бы тип курсора вы не решили использовать для выбора одной строки, спрячьте этот курсор в функцию. Разместив слой PL/SQL-кода между логикой приложения и нижележащим SQL, вы даете себе возможность изменить реализацию запроса без изменения кода приложения. С использованием этого подхода программа change_team будет выглядеть следующим образом:
CREATE OR REPLACE PROCEDURE change_team (
id_in IN team_member.id%TYPE
,new_team_id_in
IN team_member.team_id%TYPE
)
IS
l_team_member team_member%ROWTYPE;
BEGIN
l_team_member :=
team_member_qp.onerow (id_in);
team_member_qp - это запросный пакет для team_member, инкапсулирующий широкий диапазон запросной логики для таблицы, включая функцию onerow. Внутри функции onerow я могу выбирать явный или неявный курсор – или даже посмотреть, имеет ли смысл кэшировать данные из team_member в коллекцию, объявленную на уровне пакета. Дело в том, что этот момент реализации не влияет на программиста приложения, поэтому он может быть изменен, и при этом не возникнет никакого волнового эффекта.
"Избегайте пакетов"
Я питаю большое уважение к администраторам баз данных Oracle в общем. Они знают чрезвычайно много об архитектуре и внутреннем устройстве Oracle, и они должны быть в курсе всех изменений, которые приходят с каждой новой версией. Однако, если администратор не знаком с последними разработками от Oracle или неправильно понимает некоторые аспекты архитектуры, он может настаивать на некоторых правилах, которые не имеют смысла.
Например, когда администратор говорит разработчику, что нужно избегать пакетов, потому что они используют слишком много памяти, он показывает свое невежество в текущей архитектуре PL/SQL. Конечно, в любом мифе или легенде есть доля истины, и этот миф не является исключением.
Это правда, что когда вы вызываете одну программу пакета, частично откомпилированный код всего пакета загружается в память. Поэтому, если вы небрежно создавали свой пакет, вы можете использовать слишком много памяти по следующей причине: допустим, что я создал большой пакет BIGPKG, содержащий 100 программ, которые все вместе занимают 250K памяти, и эти программы относятся к различным областям функциональности. Предположим затем, что в своем приложении я вызвал программу BIGPKG.ONETHING, но не использовал никакой другой программы этого пакета. Если ONETHING занимает 20K памяти, я использую остальные 230К системной глобальной области (SGA) впустую.
Поэтому здесь есть потенциальная возможность нерационального использования памяти – но это не является недостатком пакетов. Это следствие плохого дизайна пакета и приложения. На самом деле, если тщательно проектировать свои пакеты (смотри следующие рекомендации), можно улучшить использование памяти при размещении вашего кода в SGA.
При построении приложения, основанного на PL/SQL, я предлагаю руководствоваться следующими рекомендациями:
- Поместите весь свой код в пакеты; избегайте отдельных процедур и функций. (Да, рекомендация прямо противоположная мифу). Даже если в вашем пакете сейчас есть только одна программа, потом вы, возможно, подумаете о другой, с похожей функциональностью, и тогда у вас будет место, куда ее можно положить.
- Создавайте большое количество маленьких, узко специализированных пакетов. Например, не имеет смысла создавать пакет UTILITIES, который является свалкой для всего подряд. Вместо этого создайте несколько более узко определенных пакетов, таких как string_utils, date_utils, constants_pkg, и config_pkg.
- Убедитесь, что имена ваших пакетов и программ точно отражают их содержание. Тщательно выбранные имена могут составлять единственную разницу между приложением, которое легко понять и поддерживать, и приложением, которое является спутанным клубком темных элементов.
Как не пустить мифы в свой код
Остался большой вопрос: Как вы собираетесь избежать создания новых мифов в своем коде?
Существуют два основных ответа на этот вопрос: право сомнения и инкапсуляция.
Право сомнения. Если ваш DBA говорит вам, что нужно избегать определенных областей функциональности PL/SQL из-за исторических причин, таких как “Однажды была ошибка…”, или призывает вас поверить общим благоразумным утверждениям типа “Это требует слишком много памяти”, проверьте сами. Есть ли еще эта ошибка или она была исправлена? Oracle предлагает отличный ресурс в MetaLink , чтобы помочь вам ответить на подобные вопросы.
Если ваши вопросы больше связаны с производительностью или функциональностью, можно написать свои собственные тесты. В конце концов, это не философские вопросы, это технические утверждения, которые можно проверить и либо подтвердить, либо опровергнуть. Одна из приятных сторон программирования (на самом деле, основная причина, по которой я верю, что написание кода может иногда казаться положительной привычкой) заключается в том, что мы работаем с закрытой системой. Компьютер – это машина, которая следует инструкциям. Здесь нет никаких тайн души, и PL/SQL-машина никогда не солжет вам.
Поэтому, если кто-либо утверждает, что пакеты требуют много памяти, запустите свои собственные тесты, и проанализируйте использование памяти. В листинге 1 представлена програма, которой можно воспользоваться, чтобы посмотреть использование UGA и PGA памяти для конкретной сессии, что может быть полезным для разработчиков PL/SQL, поскольку программные данные, такие как коллекции, размещаются в PGA.
Если кто-либо утверждает, что определенная техника, такая как неявные курсоры, является медленной или медленее, чем другой подход, напишите свою программу, чтобы сравнить различные подходы. Можно использовать SET_TIMING ON в SQL*Plus или воспользоваться преимуществами функций пакета DBMS_UTILITY (даже GET_TIME или GET_CPU_TIME—последними новинками в Oracle 10g), которые помогут вам вычислить истекшее время с точностью до долей секунды. Например, в листинге 2 , содержится спецификация для объектного типа, который позволит вам запускать или останавливать таймеры внутри вашей программы.
Инкапсуляция. Когда имеешь дело с мифологическим кодом, вторым важным механизмом является инкапсуляция. Когда вы инкапсулируете, вы создаете слой кода между двумя частями программного обеспечения, обычно между логикой приложения и какой-либо нижележащей функциональностью. Если у вас есть повод думать, что в будущем эта нижележащая функциональность может измениться, спрячьте ее в функцию или процедуру, и затем вызовите эту программу. Когда функциональность изменится (например, ошибка будет исправлена), вы просто замените старую функциональность на новую, и мифологический код исчезнет до того, как он станет мифом.
Поскольку вы не меняли внешний вид этой программы – ее имя и список параметров – ни одна из программ кода уровня приложения не будет затронута. Кстати, такой процесс изменения внутреннего содержания программы без изменения ее внешнего интерфейса известен в мире программирования как рефакторинг.
Инкапсуляция обходных путей или патчей для ошибок является особенно важной, и существуют некоторые специальные шаги, которые необходимо предпринять для этого вида инкапсуляции. Давайте вновь вернемся к ошибке в optimal_plan, представленной в начале этой статьи.
Ранее я предостерегал разработчиков от выставления напоказ обходных путей в своем коде, то есть от создания строк такого типа:
IF opt_info.rate_level IS NULL
AND opt_info.rate_type IS NULL
THEN
-- Не оптимальный план!
Давайте посмотрим, как инкапсуляция может помочь нам в такой ситуации. Вот альтернативный набор шагов, которым стоит следовать при реакции на подобную ошибку:
- Создайте отдельный пакет для хранения обходных путей для пакета analyze_rates. Давайте назовем его analyze_rates_wa.
- Создайте функцию в этом обходном пакете, которая основана на optimal_plan, имеет то же имя и вызывает эту функцию, но, кроме того, реализует обходной путь для ошибки в is_optimal. Эта обходная функция работает так, как должна работать исходная функция.
- Уставновите правило для своих разработчиков приложения, что analyze_rates.optimal_plan никогда не должна вызываться, и что вместо нее должна вызываться analyze_rates_wa.optimal_plan. Обратите внимание, что эти изменения в существующем коде можно произвести, тщательно выполнив глобальный поиск и замену.
- В обходной функции точно документируйте, что является неверным, и как это исправить.
- Когда поставщик исправит или обновит свой код, вы можете сделать одно из двух:
переключить ссылки с analyze_rates_wa.optimal_plan обратно на analyze_rates.optimal_plan, или, если остались еще ошибки, оставить вызов обходной программы в своем коде, но изменить внутренее содержание программы , чтобы использовать новую функциональность поставляемого программного обеспечения.
В листинге 3 представлен мой пример реализации обходного пакета для функции optimal_plan. Я включил комментарии в эту функцию, объясняющие суть проблемы, сущность обходного пути и как обновить код, когда ошибка будет исправлена.
Предположим, что ошибка будет исправлена через два года после создания приложения. Первоначальная команда разработчиков давно разошлась. Благодаря вашему благоразумию при написании обходного пакета, разработчик, теперь поддерживающий приложение, сможет легко и уверенно обновить код, чтобы отразить новую реальность, без необходимости бороться с малопонятными обходными путями, которые потом могут стать мифом.
Код без мифов легко поддерживать
Программы, которые вы пишите, обычно имеют более долгий жизненный ресурс, чем кто-либо может предположить. И чем больше кода вы пишите, тем больше уходит в производство и в режим поддержки. Если писать свои программы неаккуратно, вы (или тот, кто унаследует ваши программы) впоследствии будете иметь дело со сложными, запутанными и хрупкими приложениями.
Мифологический код – строки PL/SQL, которые представляют ложную картину – только ухудшают ситуацию. Чем больше вы сделаете сегодня, чтобы удалить мифы и избегать их в своем коде, тем лучше будет всем.
Стивен Фернстайн ( steven@stevenfeuerstein.com ) является специалистом в языке Oracle PL/SQL. Он является автором или соавтором девяти книг по PL/SQL (все - издательства O'Reilly & Associates), включая Oracle PL/SQL Best Practices и Oracle PL/SQL Programming. Фернстайн в настоящее время разрабатывает новый активный обучающий инструментарий для разработчиков, называемый Qnxo , предлагает обучение PL/SQL, и является старшим технологическим консультантом компании Quest Software
|