Oracle-利用存储过程将Excel表格的表结构生成建表语句

在日常开发过程中,有时会遇到上游只提供给我们表结构文档,并不能直接提供给我们建表语句,这样建表过程所耗费的时间就会大大增加,如果只是几张表,那随手写个create table可能就能完成任务,可是如果有几十张、几百张甚至上千张表的话,如果再去手动建表的话,那显然不太现实,一般情况下,这种可能都是通过建模工具进行建表,将表结构的相关文档导入建模工具,最后逐个生成建表语句,在近期的项目中,我手上有个建表的任务,我就想能不能通过写一个存储过程,将表结构文档生成建表语句,这样既能实现需求,又能提升一下自己的SQL技能,说干就干~

目录

1.建表

2.准备数据

2.1最理想的数据源

2.2常见的数据源

3.处理数据

生成建表语句的思路:

4.利用存储过程生成建表语句

5.执行建表语句完成建表


1.建表

我的想法是需要建三张表:

TABLE_INFO为Excel中的源数据,TABLE_INFO_TEMP为过程临时表,CREATE_TABLE为最终生成的建表语句和注释语句


create table TABLE_INFO
(
  id          number,
  db_type     VARCHAR2(10)    default 'oracle',
  tab_name    VARCHAR2(100),
  col_name    VARCHAR2(1000),
  col_type    VARCHAR2(1000),
  col_long    VARCHAR2(1000),
  col_comment VARCHAR2(4000)
);

comment on column TABLE_INFO.id
  is '数据序号';
comment on column TABLE_INFO.db_type
  is '数据库类型';
comment on column TABLE_INFO.tab_name
  is '表名';
comment on column TABLE_INFO.col_name
  is '字段名';
comment on column TABLE_INFO.col_type
  is '字段类型';
comment on column TABLE_INFO.col_long
  is '字段长度';
comment on column TABLE_INFO.col_comment
  is '字段注释';



create table TABLE_INFO_TEMP
(
  id            number,
  db_type       VARCHAR2(10) default 'oracle',
  tab_name_full VARCHAR2(100),
  tab_name_eng  VARCHAR2(100),
  tab_name_cn   VARCHAR2(100),
  col_name      VARCHAR2(1000),
  col_type      VARCHAR2(1000),
  col_long      VARCHAR2(1000),
  col_comment    VARCHAR2(4000)
);

comment on column TABLE_INFO_TEMP.id
  is '源表数据序号';
comment on column TABLE_INFO_TEMP.db_type
  is '数据库类型';
comment on column TABLE_INFO_TEMP.tab_name_full
  is '源表表名如:emp员工表,员工表emp等,插入后不变,以便后期与源表查询对应';
comment on column TABLE_INFO_TEMP.tab_name_eng
  is '英文表名';
comment on column TABLE_INFO_TEMP.tab_name_cn
  is '中文表名';
comment on column TABLE_INFO_TEMP.col_name
  is '字段名';
comment on column TABLE_INFO_TEMP.col_type
  is '字段类型';
comment on column TABLE_INFO_TEMP.col_long
  is '字段长度';
comment on column TABLE_INFO_TEMP.col_comment
  is '字段注释';


create table CREATE_TABLE
(
  tab_name    VARCHAR2(100),
  create_sql  VARCHAR2(2000),
  tab_comment clob,
  create_date DATE default sysdate
);

comment on column CREATE_TABLE.tab_name
  is '表名';
comment on column CREATE_TABLE.create_sql
  is '建表语句';
comment on column CREATE_TABLE.tab_comment
  is '中文表名及字段注释';
comment on column CREATE_TABLE.create_date
  is '创建时间';

2.准备数据

2.1最理想的数据源

如果源数据能在Excel中很简单的处理成这样,那是最好的,很快就可以按照表名对应的字段名生成建表语句

1oracle表名字段名字段类型字段长度字段注释
2oracleemp员工表emp_nonumber10员工编号
3oracleemp员工表emp_namevarchar230员工姓名
4oracleemp员工表salnumber10,2工资
5oracle表名字段名字段类型字段长度字段注释
6oracledept部门表dep_nonumber10部门编号
7oracledept部门表dep_namevarchar220部门名称
8oracledept部门表locvarchar210地址

2.2常见的数据源

但是大多数情况下,应该是不会有这么理想的情况,比如我现在手里的数据就是下面这样的,我下面的数据处理,主要也是以我下面这样的数据格式进行处理,但是会兼容上面的理想数据源的情况。

1.emp员工表
字段信息:
字段代码数据类型主键备注
emp_nonumber(10)员工编号
emp_namevarchar2(30)员工姓名
salnumber(10,2)工资
2.dept部门表
字段信息:
字段代码数据类型主键备注
dep_nonumber(10)部门编号
dep_namevarchar2(20)部门名称
locvarchar2(10)地址
3.emp_temp员工临时表
字段信息:
字段代码数据类型主键备注
emp_nonumber(10)员工编号

3.处理数据

1.将整理好的Excel数据插入到数据库中,我这里直接用PLSQL插入

 

 插入数据后可以看到,字段数据的顺序和想象中的不太一样,因为我这边的表结构中,表名并没有单独放在一列,而是和字段名字在同一列,分两行,调整一下,在Excel中字段名前面再多加一列,然后再重新插入数据

数据源差不多就这样就可以了,接下来我们需要做的就是利用存储过程对数据进行处理,需要处理的有以下几个问题:

1.在table_name字段中插入对应的表字段的表名,因为我这边的数据是表名和字段名放在同一列,也就是说我需要通过判断,把所有的表名从col_name这一列取出来,插入到table_name字段;

2.将col_type中的字段长度取出来,然后插入到col_long中去,然后col_type中只保留字段的字段类型;

3.删除表中的垃圾数据,如:Excel中的表头字段信息,字段代码,索引信息,索引名称,索引等这些数据,还要在提取完表名之后删除表中的表名数据。

简单点来说就是将数据处理成2.1步骤中理想状态的数据就可以,在处理源数据时,我插入了一个编号,每一条数据都有一个固定的编号,这个可以在Excel中就插入,也可以在插入数据库表后自行插入,目的是为了在处理数据的过程中保证数据和源数据始终都保持一致,也可以在发生其他错误时精准的定位到错误数据。

最后处理完的数据是这样

生成建表语句的思路:

利用for 循环遍历获取表名table_name_eng,表名与获取到的表名一致的则为同一表中的字段数据,再循环拼接建表语句,将字段名称,字段类型,字段长度等进行拼接,完成建表语句,最后循环拼接表字段注释。

4.利用存储过程生成建表语句

create or replace procedure create_table_by_excel(
in_create_tab     in varchar2,                --批量建表时需要返回的指定表的建表语句的表名
o_create_sql     out varchar2)                --批量建表时需要返回的指定表的建表语句
authid current_user
as
    v_count                number(5);            --统计表是否存在
    sql_statement          varchar2(4000);       --存储SQL
    sql_statement2         varchar2(4000);       --存储SQL
    sql_comment            clob;                 --注释
    sql_comment2           clob;                 --注释  字段注释过多会导致4000的长度超长
    table_name_eng         varchar2(1000);       --英文表名
    table_name_cn          varchar2(1000);       --中文表名
    create_sql_comment     varchar2(4000);       --存储返回的建表语句

begin
    v_count                := 0;       --数量
    sql_statement          :='';       --存储每张表建表语句
    sql_statement2         :='';       --存储每个字段建表语句
    sql_comment            :='';       --存储每张表注释
    sql_comment2           :='';       --存储每个字段注释
    table_name_eng         :='';       --英文表名
    table_name_cn          :='';       --中文表名

/*
还未解决的问题:
1.字段类型中不是Oracle类型的需要删除
2.字段注释存在同一个字段,多行的情况
建表语句
TABLE_INFO为Excel中的源数据,TABLE_INFO_TEMP为过程临时表,CREATE_TABLE为最终生成的建表语句和注释语句
表字段的顺序可以根据实际情况进行调整,怎么方便怎么来

create table TABLE_INFO
(
  id          number,
  db_type     VARCHAR2(10)    default 'oracle',
  tab_name    VARCHAR2(100),
  col_name    VARCHAR2(1000),
  col_type    VARCHAR2(1000),
  col_long    VARCHAR2(1000),
  col_comment VARCHAR2(4000)
);
comment on column TABLE_INFO.id
  is '数据序号';
comment on column TABLE_INFO.db_type
  is '数据库类型';
comment on column TABLE_INFO.tab_name
  is '表名';
comment on column TABLE_INFO.col_name
  is '字段名';
comment on column TABLE_INFO.col_type
  is '字段类型';
comment on column TABLE_INFO.col_long
  is '字段长度';
comment on column TABLE_INFO.col_comment
  is '字段注释';
示例数据1(理想状态,如果源数据在Excel能处理成这样最好,然后直接粘贴到TABLE_INFO表中)
id      db_type    tab_name        col_name       col_type      col_long      col_comment
1       oracle     emp员工表       BEG_DATE       CHAR          10            开始日期
2       oracle     emp(员工表)   END_DATE       CHAR          10            失效日期
3       oracle     (emp)员工表     id             VARCHAR2      50            id
示例数据2(大多数情况可能只能处理成这样,处理成这样按字段插入源表可以,后面会进行处理)
db_type       tab_name           col_name       col_type        col_long        col_comment
                                 emp员工表
                                 emp_no         number                          员工编号
                                 emp_name       varchar2(10)                    员工姓名
                                                 sal            number(20,8)                    员工工资
                                 dept部门表
                                 dept_no        number                          部门编号
create table TABLE_INFO_TEMP
(
  id            number,
  db_type       VARCHAR2(10) default 'oracle',
  tab_name_full VARCHAR2(100),
  tab_name_eng  VARCHAR2(100),
  tab_name_cn   VARCHAR2(100),
  col_name      VARCHAR2(1000),
  col_type      VARCHAR2(1000),
  col_long      VARCHAR2(1000),
  col_comment    VARCHAR2(4000)
);
comment on column TABLE_INFO_TEMP.id
  is '源表数据序号';
comment on column TABLE_INFO_TEMP.db_type
  is '数据库类型';
comment on column TABLE_INFO_TEMP.tab_name_full
  is '源表表名如:emp员工表,员工表emp等,插入后不变,以便后期与源表查询对应';
comment on column TABLE_INFO_TEMP.tab_name_eng
  is '英文表名';
comment on column TABLE_INFO_TEMP.tab_name_cn
  is '中文表名';
comment on column TABLE_INFO_TEMP.col_name
  is '字段名';
comment on column TABLE_INFO_TEMP.col_type
  is '字段类型';
comment on column TABLE_INFO_TEMP.col_long
  is '字段长度';
comment on column TABLE_INFO_TEMP.col_comment
  is '字段注释';
create table CREATE_TABLE
(
  tab_name           VARCHAR2(100),
  create_sql         VARCHAR2(2000),
  tab_comment        VARCHAR2(4000),
  create_sql_comment VARCHAR2(4000),
  create_time        DATE default sysdate
)
-- Add comments to the columns 
comment on column CREATE_TABLE.tab_name
  is '表名';
comment on column CREATE_TABLE.create_sql
  is '建表语句';
comment on column CREATE_TABLE.tab_comment
  is '中文表名及字段注释';
comment on column CREATE_TABLE.create_time
  is '创建时间';
*/

    --清空临时表数据,create_table中已经生成的历史建表语句可删可不删,有时间字段可以区分
    --execute immediate 'truncate table create_table';
    --execute immediate 'truncate table table_info_temp';

    --判断是否建表,这里简单判断中间表
    select count(1) into v_count from all_tables t where t.TABLE_NAME in('TABLE_INFO_TEMP');

    if v_count = 0 then
      return;
    end if;


    --准备数据,处理数据,将源表数据处理成标准数据

    --清空临时表
    execute immediate'truncate table table_info_temp';
    
    --清空存储建表语句的表
    execute immediate'truncate table create_table';

    --将原始数据插入到临时表,利用序号对应,防止数据插入后顺序错乱
    insert into table_info_temp
    select t.id, t.db_type, trim(t.tab_name), trim(t.tab_name), trim(t.tab_name), trim(t.col_name), trim(t.col_type), trim(t.col_long), trim(t.col_comment) from table_info t order by t.id;

    --删除表中的excel表头及无效数据
    delete from table_info_temp t
     where instr(t.col_name, '列名') > 0    --删除字段中存在该关键字的数据
        or instr(t.col_name, '数据类型') > 0   --删除存在索引的情况,有逗号,无括号,如:col_1,col_2
        or instr(t.col_name, '说明') > 0   --删除存在索引的情况,有逗号,无括号,如:col_1,col_2
        or (instr(t.col_type, ',') > 0 and instr(t.col_type, '(') = 0)
        or (t.col_name is null and t.col_type is null and t.col_comment is null)
        or (length(t.col_name) - length(replace(t.col_name, '.')) = 2 and t.col_type is null);

    --判断col_type中的数据是否包含长度,如varchar2(10),需要将类型和长度拆分开来
    select count(1) into v_count from table_info_temp t where instr(t.col_type, '(') > 0;
    if v_count > 0 then
      update table_info_temp t  --1.先将长度字段清空,防止有垃圾数据
         set t.col_long = null;

      update table_info_temp t  --2.将字段长度从字段类型里面拆出来,放入长度字段,这里的前提是数据是类型+(长度),如:varchar2(10),number(10,2)
         set t.col_long = substr(t.col_type, instr(t.col_type, '(') + 1, length(t.col_type) - instr(t.col_type, '(') - 1)
       where instr(t.col_type, '(') > 0;

      update table_info_temp t  --3.修改字段类型里面的数据,去掉长度,保留类型
         set t.col_type = substr(t.col_type, 1, instr(t.col_type, '(') - 1)
       where instr(t.col_type, '(') > 0;
    end if;

/*    --如果表名和字段名在同一列,需要将表名从字段名那一列中分离出来,如:
    table_name_eng       col_name       col_type
                         emp
                         emp_no         number
                         emp_name       varchar2(10)
                         dept
                         dept_no        number
    */


    --如果存在表名和字段名放在同一列的情况,先将表名提取出来
    select count(1) into v_count from table_info_temp t where t.tab_name_eng is null;

    if v_count > 0 then
      update table_info_temp t   --表名存在字段名称中,则字段名称不为空,字段类型为空
         set t.tab_name_full = trim(t.col_name),
             t.tab_name_eng = trim(t.col_name),
             t.tab_name_cn = trim(t.col_name)
       where t.col_name is not null and t.col_type is null;

       merge into table_info_temp t
       using(
         select id, nvl(tab_name_full, lag(tab_name_full ignore nulls) over(order by id)) table_name from table_info_temp) q
         on (t.id = q.id)
       when matched then
         update
            set t.tab_name_full = q.table_name,
                t.tab_name_eng = q.table_name,
                t.tab_name_cn = q.table_name;

       --删除字段名中表名的数据
       delete from table_info_temp t where trim(t.tab_name_eng) = trim(t.col_name);
     end if;

    --不需要长度的字段类型,如 INTEGER, clob, date等
    update table_info_temp t
       set t.col_long = null
     where lower(trim(t.col_type)) in('integer', 'clob', 'blob', 'date', 'timestamp');

    --处理字段名称中的空格
    update table_info_temp t
       set t.col_name = replace(t.col_name, ' ', '')
     where t.col_name like '% %';

    --若表名中存在括号,先将括号去掉,考虑括号为中文或英文情况
    update table_info_temp t
       set t.tab_name_eng = replace(replace(replace(replace(t.tab_name_cn, '(', ''), ')', ''), '(', ''), ')', ''),
           t.tab_name_cn = replace(replace(replace(replace(t.tab_name_cn, '(', ''), ')', ''), '(', ''), ')', '')
     where instr(t.tab_name_cn, '(') > 0
        or instr(t.tab_name_cn, '(') > 0
        or instr(t.tab_name_eng, '(') > 0
        or instr(t.tab_name_eng, '(') > 0;


    --删除表名中的中文字符存到tab_name_eng
    update table_info_temp t
       set t.tab_name_eng = regexp_replace(ASCIISTR(t.tab_name_full),'\\[[:alnum:]]{4}','');

    --删除表名中的英文字符存到tab_name_cn,用于表名注释
    update table_info_temp t
       set t.tab_name_cn = replace(t.tab_name_full, t.tab_name_eng, '');

    --去除表名中的序号,如有的话,如:1.table_name,2.table_name
    update table_info_temp t
       set t.tab_name_eng = substr(t.tab_name_eng, instr(t.tab_name_eng, '.') + 1);

    --如果表名的中文名存在多个词根,如:emp_e1员工表_测试,英文表名会多个_
    update table_info_temp t
       set t.tab_name_eng = substr(t.tab_name_eng, 1, length(t.tab_name_eng) - 1)
     where substr(t.tab_name_eng, -1, 1) = '_';

    --默认为Oracle类型
    update table_info_temp t
       set t.db_type = 'oracle'
     where t.db_type is null;

    --非Oracle数据库需要处理字段类型与Oracle对应
    select count(1) into v_count from table_info_temp t where lower(t.db_type) = 'oracle';

    if v_count = 0 then
    --MySQL,DB2等库表结构生成Oracle建表语句需先进行数据类型处理
    update table_info_temp t
       set t.col_type = 'varchar2'
     where t.db_type <> 'oracle' and lower(t.col_type) in('varchar', 'char', 'bit', 'character', 'tinyint');


    update table_info_temp t
       set t.col_type = 'number'
     where t.db_type <> 'oracle' and lower(t.col_type) in('int', 'bigint', 'smallint', 'double', 'decimal');


    update table_info_temp t
       set t.col_type = 'date'
     where t.db_type <> 'oracle' and lower(t.col_type) in('datetime', 'timestamp');


    update table_info_temp t
       set t.col_type = 'clob'
     where t.db_type <> 'oracle' and lower(t.col_type) in('text', 'longtext');


    --非Oracle库字符串类型扩大到2倍
    update table_info_temp t
       set t.col_long = to_number(t.col_long) * 2
     where lower(t.col_type) = 'varchar2' and lower(db_type) <> 'oracle';


    --非Oracle库number类型超过38时,取38
    update table_info_temp t
       set t.col_long = 38
     where lower(t.col_type) = 'number' and to_number(substr(trim(t.col_long), 1, 2)) > 38 and lower(db_type) <> 'oracle';

    end if;


    --如果处理完还存在其他不规范的数据,数量不多的话可以手动处理
    /*  比如我遇到的两种特殊情况,
    1.一种是字段注释存在同一字段的注释在Excel中对应多行,字段名存在多行单元格合并
    2.另一种是存在建索引的情况,把建索引的数据和建表的混在一起,都存在col_type字段中,此种情况暂时先将索引删除,批量建索引后期找时间再更新

    */


    --删除非Oracle数据类型的数据(主要针对数据中有单列索引在上面中无法处理,上面可能会被当成字段名处理)
    delete from table_info_temp t
     where lower(t.col_type) not in('char', 'varchar2', 'nvarchar2', 'clob', 'blob', 'nclob', 'long', 'number', 'date', 'timestamp');   --常见的数据类型基本上就这么多,其他的有需要可以自己加


    commit;


    for item in(select tab_name_eng, tab_name_cn from table_info_temp group by tab_name_eng, tab_name_cn)
    loop
        --表名赋值
        table_name_eng := item.tab_name_eng;
        table_name_cn := item.tab_name_cn;


        ------------------------------查看表是否存在,存在删除(begin)-------------------------------------------
        --查看表是否存在
        sql_statement := 'select count(*) from all_tables a where a.table_name = upper(''' || table_name_eng || ''')';
        execute immediate sql_statement into v_count;
        --如果表存在
        if v_count > 0 then
            --execute immediate 'create table ' || table_name_eng || '_bk' || 'as select * from ' || table_name_eng;
            execute immediate 'drop table ' || table_name_eng;
        end if;
        ------------------------------查看表是否存在,存在删除(end)---------------------------------------------

        --清空sql_statement
        sql_statement := '';
        sql_statement2 := '';
        sql_comment :='';
        sql_comment2 :='';

        --------------------------------------------创建表(begin)-----------------------------------------------
        sql_statement := sql_statement || 'create table '|| table_name_eng || '( ';
        for details in(select col_name, col_type, col_long, col_comment from table_info_temp  where upper(tab_name_eng) = upper(table_name_eng))
        loop
            --拼接创建表语句
            sql_statement2 := sql_statement2 || details.col_name || ' ' || details.col_type || '(' || details.col_long || '), ';

            --拼接字段注释
            sql_comment2 :='';
            sql_comment2 := sql_comment2 || 'comment on column ' || table_name_eng || '.' || details.col_name || ' is ' ||details.col_comment|| '''' ||';';

            sql_comment := sql_comment || chr(13) || sql_comment2;

            --dbms_output.put_line(sql_comment);


        end loop;

        --去掉最后多拼接的逗号
        sql_statement := sql_statement || substr(sql_statement2, 0, length(sql_statement2)-2);

        --后面的右括号
        sql_statement := sql_statement || ');';

        --拼接表名注释
        sql_comment := 'comment on table ' || table_name_eng || ' is ' || '''' || table_name_cn || '''' || ';' || sql_comment;


        --MySQL库表结构生成Oracle建表语句需先进行数据类型处理
        sql_statement := replace(replace(sql_statement, 'date(0)', 'date'), 'clob(0)', 'clob');


        --优化建表语句,没有长度的字段类型()需要删除
        sql_statement := replace(sql_statement, '()', '');


        --建表语句留存
        insert into create_table(tab_name, create_sql, tab_comment, create_sql_comment) values(table_name_eng, sql_statement, sql_comment, sql_statement || chr(13) || sql_comment);


        commit;


        -- 执行创建语句和注释语句
        --sql_statement := replace(sql_statement, ';', '');
        --execute immediate sql_statement;
        --execute immediate sql_comment;



       --------------------------------------------创建表(end)-----------------------------------------------
     end loop;
     

    --返回建表语句+字段注释
      --返回建表语句长度
      if in_create_tab is not null then
        select length(t.create_sql_comment) into v_count from create_table t where t.tab_name = in_create_tab;
      else
        v_count := length(sql_statement || chr(13) || sql_comment);
      end if;

      --判断建表语句的字符长度,超过1900的不展示,去表里面查询,不超过1900则展示
      if in_create_tab is not null and v_count <= 1900 then
        select t.create_sql_comment into o_create_sql from create_table t where t.tab_name = in_create_tab;
        elsif in_create_tab is null and v_count <= 1900 and v_count > 0 then
        o_create_sql := sql_statement || chr(13) || sql_comment;
        elsif in_create_tab is not null and v_count > 1900 then
        o_create_sql := 'select * from create_table where tab_name = '|| '''' || in_create_tab || ''';' ;
      else
        o_create_sql := 'select * from create_table where create_time >= trunc(sysdate);';
      end if;


/*注:如需修改建表语句格式
    1.将create_sql复制到notepad++,用正则表达式将“,”替换为,\n;
    2.将tab_comment复制到notepad++,将“"”替换为空;*/


end create_table_by_excel;

5.执行建表语句完成建表

这里因为我的库里已经有测试的表了,所以改了个表名,注释这个字段我这里用的是clob类型,因为我这边处理的表里面有几百个字段的表,varchar2最多只能存4000个字符,可能会存在超长的情况,所以这里我用了clob类型。

生成建表语句以后,可以将建表语句复制出来,然后执行即可。 

如果字段过多觉得一行不好看,或者建表语句需要作为文档上传这种,可以将建表语句拷贝到notepad++之类的工具中进行处理,比如我这里用notepad++处理,处理完之后看起来可能会稍微好看一些。

 如果需要使用脚本执行建表语句及注释的话,可以自己在过程中拼写一个执行语句,后续如果有谁需要的话,也可以留言我这边更新~

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

 得不到的永远最美

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值