有一个APEX小应用需要提供Excel数据上传功能,并且要对上传的数据进行后续加工处理,于是就有了下面这段有点通用的代码。

APEX 4.0中上传的存储位置有两个选择,一个是集中式的WWV_FLOW_FILES,一个是自定义的表。对于前者我不喜欢,所有应用上传的文件都存储在一个地方,有些时候管理起来不够方便;如果是自定义的表,每个应用单独存放,那么不论在迁移还是某些维护都相对简单,当然,对于上传功能的设计而言就需要多花几分钟时间了。下面是自定义表的一个例子。

1. 先创建存储表

attach_data blob 二进制数据
attach_mimetype varchar2(255) 文件类型
attach_filename varchar2(255) 文件名
attach_last_update date 上传时间
attach_charset varchar2(128) 字符集
attach_id number 文件ID

一般而言,最好将该表最小化,包含必须的几个字段即可,如果需要和其他表关联,则可以通过ATTACH_ID关联。

2. APEX页面中增加File…上传的item,属性设置如下:

3. 添加提交表单的按钮,提交时触发一次Automatic Row Processing (DML)的Process,设置表名和主键,系统会自动将相关数据(包括上传的文件)插入对应的表中。关于主键ID的自动生成,可以在After header位置增加一个自动fetch row的设置即可。

4. 文件上传功能本身是非常简单的,接下来是针对该上传文件的后续处理。在顺序上,要先有处理上传的Process(就是第3步),然后再有后续处理的Process。手工在Page Process处再添加一个名为Parsing Attachment(名字随意)的Process,在Source处理加入匿名PL/SQL代码。例如我喜欢直接调用写好的包,那就写上:

process_upload.process_file_upload;

这里需要用户首先对Excel做一个另存为文本文件的操作,目前为止,我还没发现可以用PL/SQL来直接操作二进制Excel文件。如果有必要,可以考虑使用Java Procedure对Excel文件进行处理。

附上几段代码:

PROCEDURE process_file_upload IS
    l_blob_data       BLOB;
    l_blob_len        NUMBER;
    l_position        NUMBER;
    l_raw_chunk       RAW(2);
    l_raw_line        RAW(32767);
    l_line            VARCHAR2(32767) DEFAULT NULL;
    l_array           wwv_flow_global.vc_arr2;
    l_first_line_flag BOOLEAN;
  BEGIN
    SELECT s.attach_data
      INTO l_blob_data
      FROM sw_detail_attachment s
     WHERE s.attach_id = v('P7_ATTACH_ID');

    DELETE FROM sw_detail_attachment s
     WHERE s.attach_id = v('P7_ATTACH_ID');

    l_blob_len        := dbms_lob.getlength(l_blob_data);
    l_position        := 1;
    l_first_line_flag := TRUE;
    WHILE (l_position <= l_blob_len) LOOP
      l_raw_chunk := dbms_lob.substr(l_blob_data, 1, l_position);

      -- chr(10)
      IF l_raw_chunk = utl_raw.cast_to_raw(chr(10)) THEN
        l_line := utl_raw.cast_to_varchar2(l_raw_line);
        -- remove carriage return
        l_line  := REPLACE(l_line, chr(13), '');
        l_array := string_to_table(l_line);

        IF NOT l_first_line_flag THEN
          insert_data(l_array);
        END IF;
        l_raw_line        := NULL;
        l_first_line_flag := FALSE;
      ELSE
        l_raw_line := l_raw_line || l_raw_chunk;
      END IF;
      l_position := l_position + 1;

    END LOOP;
  END process_file_upload;

上传的文件是以TAB分割的Excel表格数据,我这里先取出文件数据放到l_blob_data中,然后针对换行符进行分割(Windows文件还需要清理掉回车符号),再对每一行使用标准函数wwv_flow_utilities.string_to_table进行解析。该函数可能存在一个问题,就是如果Excel最后一列数据为空,将导致解析后的列数缺少一列,我用下面这种方式修复该问题:

FUNCTION string_to_table(x_str IN VARCHAR2) RETURN wwv_flow_global.vc_arr2 IS
    l_col_num NUMBER := 20;
    l_array   wwv_flow_global.vc_arr2;
  BEGIN
    -- CHR(9) is tab key
    l_array := wwv_flow_utilities.string_to_table(x_str, chr(9));

    IF l_array.count = l_col_num - 1 THEN
      l_array(l_col_num) := NULL;
    END IF;

    RETURN l_array;
  END string_to_table;

Excel数据还可以采用复制粘贴的方式直接通过表单提交上去,不过,如果Excel文件内容比较复杂,存在多个标签页,则直接上传导入Oracle数据库应该还是有相当大的需求的,如果有时间做一个配置性强的上传插件的话估计下载量会不少。

Update
APEX 4.0.1 的一个bug:如果采用非WWV_FLOW_FILES表存储上传的文件,如果用IE内核的浏览器,无法上传任何数据。