📜 ⬆️ ⬇️

Creating an Excel file from a select with parameters using pure PL / SQL, as an alternative to Oracle * Reports

Goals I wanted to achieve



Stored PL / SQL procedure gets in parameters
and creates a full-fledged Excel with several tables (sheets).
I know that there are Crystal Reports and Oracle BI Publisher.
But, first of all, these are large products (with high prices ...), and Publisher, as far as I know, does not work as a separate module without Oracle Business Intelligence Enterprise Edition. And besides, it was a fairly narrow task of creating a file without layout.

At the end, I wrote one PL / SQL package that is in the database and can be called from any application. In the course of writing, I was faced with many restrictions and I want to talk about how to overcome them.

For those who doubt, I certainly could not have known this in advance, but for several years that the package has been working in a large company, I have not had problems reducing RDF of any complexity, with many triggers / formulas, in one select, praise Oraklu. On the contrary, since the select is a string parameter and can be built dynamically, this gives greater flexibility. In the parameters you can even set the name of the table.

First of all, do not judge strictly for the abundance of anglicisms (so, in my opinion, this is called), I have just been outside the Russian software community for a long time and I don’t know what these words are replacing.
')
Very often, applications written in Oracle Forms / Reports use Oracle * Reports to create an Excel file, because it is possible to use parameters and modify the selections before it is executed. Then in the trigger at the level of the output line write the output to the file. It turns out the csv file. Well, you can, of course.
If together with Excel you need to create a pdf, then there is no getting around, use Reports and do not complain about how this program got you. But after all, often you just need Excel and you don’t feel like dodging for this RDF.

So, to the point.
File options
It's simple
<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> 

Approximately such a set. I think everything is clear. Few words:

LIMIT_ROWS, LIMIT_LEN allow you to divide the resulting file in the creation process as you reach the limit values ​​by several Excel correct ones.

LITERAL_PARAMS talks about how to use execution parameters — insert values ​​or execute selections in dbms_sql with dbms_sql.bind_variable.

OUT_TYPE sets the format: Excel Workbook, CSV, HTML, XML


How to ensure the dynamism of the select with the parameters obtained in runtime


Execution options
Parameters are passed as:
  <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> 



Pre-fulfillment language

You need a certain language in which you can write instructions on what to do depending on the values ​​of the execution parameters.

Here is the language that at the end covered all my needs.

In the select text it looks like a comment (hint)
 /*!<HINT> [{]operand1[}] [ [{]operand2[}][{]operand3[}] ] ; !*/ 

The first word is hint, which defines the command
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} BC ... Z;
ROW_SUM {Total column title} BCD ... Z;
BEFORE {PL / SQL block};
TITLE {Title};
ALIAS {column_name} {alias};
AFTER {PL / SQL block};

The compilation step is that I find the command in the text, if one of the operands needs to be executed - I do it as a select (expression) from dual or as a PL / SQL block in execute immediate and replace the whole command with the result of the execution.
for example
 /*! 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} FG; !*/ /*! BOTTOM_SUM Total FG /*! IS_NOT_NULL :loc_id I ; !*/ /*! IS_NULL :loc_id H; !*/ ; !*/ 


Depending on the transmitted values, you can get any selects
  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 

or
  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 so on ...

I described this in order for you to believe that these techniques allow you to write really effective selects.
No "and (: param1 is null or table_field =: param1)"

Parsing and execution


For the sake of this section, I started writing this article. Here I will write about the experience that I have acquired, and which certainly is not needed by someone who did not go on the limits of acceptable in Orakle. For example, everyone knows that the maximum length of a text field in a table is 4000, but how many people know that the limit for concatenating a string field in an oracle is also 4000 bytes.
Everybody knows
Maybe I'm wrong, maybe it was just me who did not know.

Got select after precompilation with execution parameters. We have it in the variable l_Stmt.
Unfortunately, in PL / SQL there is no easy way to loop through select fields, as it could be done in Java. We will use the dbms_sql.parse procedure, which returns the select fields as a table, using which we will make a cycle in the future.
What do we want to do?
Perform parsing and get a list of fields with datatype.
For this we use
dbms_sql.parse
  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; 

dbms_sql.describe_columns
  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; 

We got the most important thing - the list of select fields in the PL / SQL table l_LogColumnTblInit.
This is a great DBMS_SQL package for us. Now we can organize a cycle on the select fields.
Anyone who writes in Java (including me today) will laugh at such a victory, there it has always been - the search of fields in PreparedStatement.
Now, knowing Java, I would write, maybe differently, but the basic things would not change.
By the way, here I met a limit on the size of 32K, not immediately, during operation, when they began to write serious selections. And here I was again pleased Orakl. It turns out that a long select can be divided into 256 byte portions, loaded into PL / SQL with the l_LongSelectStmt dbms_sql.varchar2s table and transferred to overload the dbms_sql.parse version.
  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; 

Now it's time to think about output formats.
Let's say our select looks like this:
  select a,b from table1 where ... 

To output in CSV format you need to write
  select a||chr(9)||b from( select a,b from table1 where ... ) 

To output in HTML format you need to write
  select '<tr><td>'||a||'</td><td>'||b||'</td></tr>' from( select a,b from table1 where ... ) 

To create the most beautiful, but also the most complex Excel Workbook format, I had to experiment with Excel. Excel Workbook is not a binary, but a text file, you can look at it and understand how everything is arranged there.
There are CSS, Workbook definitions, Worksheet, table headers. I will not go deep, it is not very difficult to understand if you have met before HTML.
In Excel Workbook, the output line will look something like this.
  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 ) 

Here, as you can see, knowledge of data types from a virtual table obtained in dbms_sql.describe_columns may be useful to us .
If we compare the types of output, we can say the following:
CSV - small in size (this is a plus), ugly, there is no possibility of several tables (spreadshhets)
HTML - medium in size, quite sexy, there is no possibility of several tables
Excel Workbook - a large file, beautiful, you can create multiple tables

Work algorithm


Cycle through fields

Moving on the table of output fields, we wrap the next field in the appropriate format tags or simply add tabulation (CSV). Now you understand how I hit about these 4000 bytes. I had to check the length of the result before merging the lines and, if it was more than 4000, I started a new output field, like this:
  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 ) 

When is built, we execute it. If the select is large, then it must be loaded into the dbms_sql.varchar2s table and executed in dbms_sql. If your DBA says that it does not tolerate literals and requires that the parameters be bind variable, then you also need to use dbms_sql with dbms_sql.bind_variable.
Otherwise, if your select fits in 32K of your varchar2 variable l_Stmt you can open the ref cursor:
  begin open l_RefCursor for l_Stmt; exception when others then v_Msg := '--After open: ' || sqlerrm; raise ParsingException; end; 

Cursor cycle

We do fetch and write in utl_file. We monitor the number of lines and the size of the output file, if necessary, complete it (nicely, Excel correctly) and start the next one.
At the end, or, if it is an Excel Workbook in a separate sheet, we display the parameters with which the report was executed.

Well, probably all by and large.
Probably now you can show the result:
image

If anyone is interested, I can tell you how I wrapped this package in another one, which ziped the file, if it was large, sent it by mail as a link or as an attachment, but the main thing is the parameter definitions and the typical input screen.

Source: https://habr.com/ru/post/263323/


All Articles