Прежде всего не судите строго за обилие англицизмов(так, по-моему, это называется), я просто давно вне русского программного сообщества и не знаю, чем заменяют эти слова.
Цели, которых я хотел достичь
- Excel, как результат селекта, текст которого процедура узнает только в runtime
- Селект перед выпонением видоизменяется в соответствии с параметрами, которые получает процедура
- Процедуре передаются параметры файла, который будет создан
- Возможность получения результирующего файла в форматах Excel Workbook,CSV,HTML,XML
Хранимая PL/SQL процедура получает в параметрах
- текст селекта
- параметры файла Excel
- параметры выполнения
и создает полноценный Excel с несколькими таблицами(sheets).
Я знаю, что есть Crystal Reports и Oracle BI Publisher.
Но, во-первых, это крупные продукты(с большими ценами…), а Publisher, насколько я знаю не работает как отдельный модуль без Oracle Business Intelligence Enterprise Edition. И кроме того, речь шла о довольно узкой задаче создания файла без layout.
В конце, я написал один PL/SQL пакет, который находится в database и может быть вызван из любой аппликации. В ходе написания я столкнулся со многими ограничениями и хочу рассказать о том, как их поборол.
Для тех — кто сомневается, я этого, конечно не мог знать заранее, но за несколько лет, что пакет работает в большой компании, у меня не было проблем свести RDF любой сложности, с многими триггерами/формулами, в один селект, хвала Ораклу. Наоборот, так как селект — стринговый параметр и его можно построить динамически, это дает большую гибкость. В параметрах можно задать даже имя таблицы.
Очень часто в аппликациях, написанных в Oracle Forms/Reports для создания файла Excel используют Oracle*Reports, потому что там есть возможность использовать параметры и видоизменять селект до его выполнения. Потом в триггер на уровне строки вывода пишут вывод в файл. Получается csv файл. Ну что же, можно и так, конечно.
Если вместе с Excel нужно создать pdf, то никуда не денешься, пользуйся Reports и не жалуйся как тебя достала эта программа. Но ведь часто нужен только Excel и городить для этого RDF как-то не хочется.
Итак, к делу.
<DIR_NAME> </DIR_NAME> <FILE_NAME> </FILE_NAME> <OUT_TYPE> </OUT_TYPE> <LIMIT_ROWS> </LIMIT_ROWS> <LIMIT_LEN> </LIMIT_LEN> <EXCEL_TITLE> </EXCEL_TITLE> <SUBTITLE></SUBTITLE> <SUBTITLE2></SUBTITLE2> <DIRECTION> ltr/rtl </DIRECTION> <CHARSET> </CHARSET> <LITERAL_PARAMS> Y/N</LITERAL_PARAMS> <DIVIDE_BY>FILES|SHEETS</DIVIDE_BY> <PARAM_TITLE> </PARAM_TITLE> <PAR_NAME_HEAD> </PAR_NAME_HEAD> <PAR_VALUE_HEAD> </PAR_VALUE_HEAD> <NOT_FOUND_MSG> </NOT_FOUND_MSG> <LONG_OUT>Y/N</LONG_OUT> <MULTI_VALUE_DELIMITER> </MULTI_VALUE_DELIMITER> <CURR_DATE_PROMPT> </CURR_DATE_PROMPT> <DEFAULT_DATE_MASK> </DEFAULT_DATE_MASK> <OUTPUT_DATE_FORMAT></OUTPUT_DATE_FORMAT> <CURRENT_SHEET></CURRENT_SHEET> <TOTAL_SHEETS></TOTAL_SHEETS>
Примерно такой набор. Думаю, тут все понятно. Несколько слов:
LIMIT_ROWS, LIMIT_LEN позволяют делить результирующий файл в процессе создания по мере достижения предельных значений на несколько Excel корректных.
LITERAL_PARAMS говорит о том, как использовать параметры выполнения — вставлять значения или выполнять селект в dbms_sql с dbms_sql.bind_variable.
OUT_TYPE задает формат: Excel Workbook,CSV,HTML,XML
Как обеспечить динамичность селекта с параметрами, получаемыми в runtime
<PARAMS> <PARAM> <NAME> </NAME> <DATATYPE> [ALPHANUMERIC|CHAR|DATE|NUMBER|AS_IS] </DATATYPE> <FORMAT_MASK>[Date format]</FORMAT_MASK> <PROMPT> </PROMPT> <LABEL> </LABEL> <VALUE> </VALUE> </PARAM> .. </PARAMS>
Язык предвыполнения
Нужен некий язык, на котором можно написать инструкции, что делать в зависимости от значений параметров выполнения.
- Получаем параметры выполнения.
- Компилируем текст селекта.
- Подаем его для выполнения следующему шагу.
Вот язык, который в конце покрывал все мои потребности
В тексте селекта это выглядит как комментарий(hint)
/*!<HINT> [{]operand1[}] [ [{]operand2[}][{]operand3[}] ] ; !*/
Первое слово — это hint, определяющий команду
VAR CHAR | NUMBER | AS_IS | DATE [date format] {PL/SQL expression};
IF_CONTINUE {PL/SQL expression}
IF_EXECUTE {PL/SQL expression}
EXPR {PL/SQL expression};
IIF_EXPR {boolean expression} {String if true} {String if false};
IS_NOT_NULL {Bind variable} {String if Bind variable is not null};
IS_NULL {Bind variable} {String if Bind variable is null};
BOTTOM_SUM {Total bottom title} B C …Z;
ROW_SUM {Total column title} B C D… Z;
BEFORE {PL/SQL block};
TITLE {Title};
ALIAS {column_name} {alias};
AFTER {PL/SQL block};
Шаг компиляции заключается в том, что я нахожу в тексте команду, если один из операндов требует выполнения — выполняю это как select (expression) from dual или как PL/SQL блок в execute immediate и заменяю всю команду на результат выполнения.
/*! VAR :Max_salary_dep number {select department_id from (select ee.department_id, sum(ee.salary) from employee ee /*! IS_NOT_NULL :emp_id {where ee.employee_id = :emp_id}; !*/ group by ee.department_id order by sum(ee.salary) desc) where rownum = 1} !*/ /*! VAR :Debug_print char 'Y' ; !*/ select e.first_name "First Name", e.last_name, d.name "Department name", j.function,e.hire_date,e.salary,e.commission /*! IS_NOT_NULL :loc_id {,l.regional_group}; !*/ from department d,employee e,job j /*! IS_NOT_NULL :loc_id {,loc l}; !*/ where e.department_id=d.department_id and e.job_id=j.job_id /*! IS_NOT_NULL :loc_id { and l.loc_id=d.loc_id}; !*/ /*! IS_NOT_NULL :hire_date { and hire_date >= :hire_date}; !*/ /*! IS_NOT_NULL :function { and j.function=upper(:function)}; !*/ /*! IIF_EXPR {:dep_id is not null} {and d.department_id = :dep_id} {and d.department_id = :Max_salary_dep}; !*/ /*! IS_NOT_NULL :emp_id {and employee_id = :emp_id}; !*/ /*! ROW_SUM {Total row} F G; !*/ /*! BOTTOM_SUM Total F G /*! IS_NOT_NULL :loc_id I ; !*/ /*! IS_NULL :loc_id H; !*/ ; !*/
select e.first_name "First Name", e.last_name, d.name "Department name", j.function,e.hire_date,e.salary,e.commission from department d,employee e,job j where e.department_id=d.department_id and e.job_id=j.job_id and d.department_id = 20
select e.first_name "First Name", e.last_name, d.name "Department name", j.function,e.hire_date,e.salary,e.commission ,l.regional_group from department d,employee e,job j ,loc l where e.department_id=d.department_id and e.job_id=j.job_id and l.loc_id=d.loc_id and hire_date >= to_date('1985-09-08','yyyy-mm-dd') and j.function=upper('SALESPERSON') and d.department_id = 30
или
select e.first_name "First Name", e.last_name, d.name "Department name", j.function,e.hire_date,e.salary,e.commission from department d,employee e,job j where e.department_id=d.department_id and e.job_id=j.job_id and hire_date >= to_date('1985-09-08','yyyy-mm-dd') and j.function=upper('SALESPERSON') and d.department_id = 30
ну и так далее…
Я это описал для того, чтобы вы поверили, что эти приемы позволяют писать действительно эффективные селекты.
Никаких " and (:param1 is null or table_field=:param1)"
Парсинг и выполнение
Ради этого раздела я затеялся писать эту статью. Здесь я напишу об опыте, который приобрел, и который наверняка не нужен тому, кто не ходил на границах допустимого в Оракле. Например, все знают, что максимальная длина текстового поля в таблице — 4000, но многие ли знают, что предел для конкатенации строкового поля в селекте в оракле тоже 4000 байтов.
Получили селект после предкомпиляции с параметрами выполнения. Он у нас в переменной l_Stmt.
Что мы хотим сделать?
Выполнить парсинг и получить список полей с datatype.
Для этого применяем
l_CursorId := dbms_sql.open_cursor; begin dbms_sql.parse(l_CursorId, substr('select * from (' || l_Stmt || ')', 1, 32765),1); exception when others then v_Msg := '--After parse: ' || sqlerrm; put_str_to_output(substr('select * from (' || l_Stmt || ')',1,32765)); raise ParsingException; end;
begin dbms_sql.describe_columns(l_CursorId, l_ColumnCnt, l_LogColumnTblInit); exception when others then v_Msg := '--After describe_columns: ' || sqlerrm; put_str_to_output(substr('select * from (' || l_Stmt || ')', 1, 32765)); raise ParsingException; end;
Мы получили самое главное — список полей селекта в PL/SQL таблице l_LogColumnTblInit.
Это для нас выполнил великий пакет DBMS_SQL. Теперь мы можем организовать цикл по полям селекта.
Тот, кто пишет на Java(в том числе и я теперешний) посмеется над такой победой, там это всегда было — перебор полей в PreparedStatement.
Сейчас, зная Java, я бы написал бы, может, по другому, но принципиальные вещи не изменились бы.
Кстати, здесь я встретил ограничение на размер селекта 32К, не сразу, в ходе эксплуатации, когда начали писать серьезные селекты. И тут меня снова порадовал Оракл. Оказывается, длинный селект можно разбить на порции 256 байт, зарузить в PL/SQL таблицу l_LongSelectStmt dbms_sql.varchar2s и передать в overload версию dbms_sql.parse.
begin dbms_sql.parse(l_CursorId ,l_LongSelectStmt ,1 ,l_LongSelectStmt.count ,false ,1); exception when others then v_Msg := '--After parse long 2: ' || sqlerrm; raise ParsingException; end;
Теперь пришло время подумать о форматах вывода.
Допустим, наш селект выглядит так:
select a,b from table1 where ...
Для вывода в формате CSV нужно написать
select a||chr(9)||b from( select a,b from table1 where ... )
Для вывода в формате HTML нужно написать
select '<tr><td>'||a||'</td><td>'||b||'</td></tr>' from( select a,b from table1 where ... )
Для создания самого красивого, но и самого сложного формата Excel Workbook, мне пришлось поэкспериментировать с Excel. Excel Workbook — это не бинарный, а текстовый файл, его можно посмотреть и понять, как там все устроено.
Там есть CSS, определения Workbook,Worksheet, заголовков таблиц. Не буду углубляться, это не очень сложно понять, если вы встречали раньше HTML.
В Excel Workbook строка вывода будет выглядеть примерно так
select '<Row> <Cell ss:StyleID="s24"><Data ss:Type="String">'||a|| '</Data></Cell>'|| '<Cell ss:StyleID="s25"><Data ss:Type="DateTime">'||b||'</Data></Cell>'|| '<Cell ss:StyleID="s24"><Data ss:Type="String">'||c||'</Data></Cell></Row>' from ( select a,b,c from table2 )
Здесь, как вы видите, нам может пригодиться знание типов данных из виртуальной таблицы, полученной в dbms_sql.describe_columns.
Если сравнивать типы вывода, то можно сказать следующее:
CSV — маленький по размеру(это плюс), некрасивый, нет возможности нескольких таблиц(spreadshhets)
HTML — средний по размеру, достаточно сексуальный, нет возможности нескольких таблиц
Excel Workbook — большой файл, красивый, есть возможность создания нескольких таблиц
Алгоритм работы
Цикл по полям
Двигаясь по таблице выходных полей, заворачиваем очередное поле в соответствующие формату тэги или просто добавляем табуляцию(CSV). Теперь вы поняли, как я ударился об эти 4000 байтов. Мне пришлось проверять перед слиянием строк длину результата и, если она была больше 4000, то начинал новое поле вывода, примерно так:
select '<Row> <Cell ss:StyleID="s24"><Data ss:Type="String">'||a|| '</Data></Cell>'|| '<Cell ss:StyleID="s25"><Data ss:Type="DateTime">'||b||'</Data></Cell>' а1, '<Cell ss:StyleID="s24"><Data ss:Type="String">'||c||'</Data></Cell></Row>' а2 from ( select a,b,c from table2 )
Когда селект построен, выполняем его. Если селект большой, то его надо загрузить в dbms_sql.varchar2s таблицу и выполнить в dbms_sql. Если ваш DBA сказал, что он не потерпит литералы и требует, чтобы параметры были bind variable, то тоже нужно использовать dbms_sql с dbms_sql.bind_variable.
Иначе, если ваш селект поместился в 32К вашей varchar2 переменной l_Stmt можете открыть ref cursor:
begin open l_RefCursor for l_Stmt; exception when others then v_Msg := '--After open: ' || sqlerrm; raise ParsingException; end;
Цикл по курсору
Делаем fetch и пишем в utl_file. Следим за количеством строк и за величиной выводимого файла, если нужно завершаем его и начинаем следующий.
Ну вот, наверно и все по большому счету.
Наверное теперь можно показать результат:
Если кому интересно, я могу рассказать, как я завернул этот пакет в другой, который зиповал файл, если он был большой, посылал его по мейлу как ссылку или как attachment, но главное, это определения параметров и типовой экран ввода.
ссылка на оригинал статьи http://habrahabr.ru/post/263323/
Добавить комментарий