繁体   English   中英

如何在 PL/SQL 中重用查询块以最小化代码样板和 object 意大利面条

[英]How to reuse a query block in PL/SQL to minimize code boilerplate and object spaghetti

各位 PL/SQL 专家,这也许是一个无法回答的问题,但也许有人有一个绝妙的解决方案。

今天我发现自己复制粘贴了一个非常冗长的 SQL 语句,以便我可以在一种情况下有条件地加入另一个表,而在另一种情况下不这样做。 显然,这让我这个程序员感到厌烦,他现在有两个 SQL 的副本需要维护:

IF <condition>
THEN
  FOR rec_data IN (SELECT <complex SQL, pages long>)
  LOOP
    PIPE ROW(....);
  END LOOP;
ELSE
  FOR rec_data IN (SELECT *
                     FROM (SELECT <same complex SQL, pages long> ) x,
                          <another table> y)
  LOOP
    PIPE ROW(....);
  END LOOP;
END IF;

如何避免拥有相同 SQL 的两个副本?

选项:

  1. 使用动态 SQL 并有条件地将主 SQL 与另一个执行附加连接的查询块一起包装。 缺点:动态 SQL 更难阅读和使用,因为所有 escaping 单引号,维护绑定变量列表等。而且,它是字符串操作,感觉就像糟糕的编码。 而且,我必须定义一个记录类型,其中包含要从中获取的所有列。 更多的工作,更多的冗余编码(我需要为几十个函数做这个,而不仅仅是一个,所以这很重要)。

  2. 创建另一个运行核心 SQL 并返回行的流水线 function,然后在我的顶部 function 中以两种不同的方式查询。 缺点:我的代码现在被拆分成一个完全不同的 object,现在我不仅要为行创建类型,而且它们必须是 SQL 类型。 很多定义只是为了一个函数的本地使用。

  3. 创建一个全局临时表并加载它的核心 SQL. Select 直接从它或 select 并有条件地加入。 缺点:现在我在我的代码 object 之外有一个用硬编码列定义定义的表,仅供该代码 object 使用。请记住,我有许多这样的函数要编写,我不想要 object 意大利面条。

  4. 为核心SQL创建视图,条件查询。 缺点:单独维护 object,加上无法将变量深入视图。

  5. 无条件地只使用较长的版本(带有条件连接的版本),但在其中使用花哨的 CASE/DECODE 来有效地禁用连接(例如DECODE(<condition>,x.join_key,NULL) = y.join_key )。 缺点:这是相当 hacky,如果您有条件地加入的附加“表”是昂贵的 PL/SQL 管道 function,那么获得性能优势可能并不那么容易。我试图避免调用 function,如果不需要 output。

所希望的是避免必须复制粘贴,避免字符串操作,并避免必须定义列定义只是为了获取只在本地需要的东西。 就像我们需要做这样的事情(从我的 PL/SQL cursor var 中读取 SQL...这是伪代码,我知道你不能按照写的那样做!)

  DECLARE
    CURSOR cur_data IS
    SELECT <complex SQL>;
  BEGIN
    IF <condition>
    THEN
      FOR rec_data IN (SELECT *
                         FROM cur_data)
      LOOP
        PIPE ROW(....);
      END LOOP;
    ELSE
      FOR rec_data IN (SELECT *
                         FROM cur_data,
                              other_table)
      LOOP
        PIPE ROW(....);
      END LOOP;
    END IF;

任何疯狂的想法?

Dynamic SQL 是处理样板代码的好方法。 在大多数编程语言中,动态代码都是有问题的,因为很难对编程语言和环境进行推理,而且字符串操作很丑陋。 Oracle 有一些功能可以缓解这些问题。

Oracle 提供了数据字典和 PL/Scope 等工具,可以更轻松地推断我们的数据库环境和代码。 SQL 对象很容易通过使用ALL_TAB_COLUMNS等视图的简单 SQL 语句来理解。

Oracle 具有可以显着清理字符串操作代码的功能。 我们可以通过组合多行字符串、替代引用机制和简单的模板系统来构建更清晰的代码,而不是无休止的连接和使用大量的引号。

多行字符串意味着简单地使用本机行尾而不是连接CHR(10)||CHR(13) (我很困惑为什么 2023 年的一些语言不支持这样一个简单的概念。)替代的引用语法允许我们指定我们自己的分隔符,比如q'....!' , q'[...]'q'<...>' - 不再有双引号。 模板不需要花哨的引擎,只需要简单的变量语法和REPLACE函数。

declare
    -- Create a SQL template with well-formatted code.
    -- The variables will be replaced later.
    -- In trivial examples, templating may need more lines of code than concatenation,
    -- but for REAL code, defining the template in one place up front is a life-saver.
    v_sql clob :=
    q'[
        insert into some_table
        select 'a' b, 'c' d, '#VALUE1#'
        from #TABLE1#
        #WHERE1#
    ]'
begin
    -- Set variables.
    -- (In real code, you may need to worry about SQL injection and the performance of
    --  using literals instead of bind variables.)
    v_value1 := 'A';
    v_table1 := 'dual';
    v_where := 'where 1=1'
    ...

    -- Replace the variables here.
    v_sql := replace(replace(replace(v_sql
        , '#VALUE1#', v_value1)
        , '#TABLE1#', v_table1)
        , '#WHERE1#', v_where);

    -- Printing the SQL is useful for debugging.
    dbms_output.put_line(v_sql);

    -- Run the SQL.
    -- (This will get more complicated for bind variables and retrieving results.)
    execute immediate v_sql;
end;
/

缺点最少的解决方案似乎使用普通 SQL 和有条件关闭的连接。 在我的例子中,它是一个昂贵的流水线 function,我并不总是需要它的 output。 所以:

SELECT *
  FROM (<very long main query>)
       LEFT OUTER JOIN (SELECT * FROM expensivefunction(in_param => 12345)) s ON 'N' = var_bypass_function

在 PLSQL 中将 var_bypass_function 设置为 Y 或 N,然后执行 cursor。我已经通过 dbms_output 跟踪验证,当连接条件始终为 false(Y=N、1=2 等)时,它会修剪整个块并绕过执行一共function。 所以没有必要把我的 SQL 放在两个不同的地方。

由于有很多条件和 SQL 的变化取决于这些条件,也许你可以创建一个小的 package 来完成它。 您可以在那里定义您的长查询(仅一次)并将其与一些嵌入变量组合以处理变化的部分。 这是您应该根据您的环境和需要进行调整的基本结构。

这是 package:

CREATE OR REPLACE 
PACKAGE REF_CURSORS AS 
--
  Procedure Init;
--
  Function Get_Cursor(p_add_select IN VARCHAR2 := 'Select base.* ', p_add_from IN VARCHAR2 := '', p_add_where IN VARCHAR2 := '') RETURN SYS_REFCURSOR;
--
  Procedure Do_It;
--
END REF_CURSORS;

...和 package 正文:

CREATE OR REPLACE
PACKAGE BODY REF_CURSORS AS
    --    variables to construct different cursors
    m_sql         VarChar2(4000) := '';
    m_select      VarChar2(2000) := '';
    m_from        VarChar2(255) := '';
    m_where       VarChar2(255) := '';
    --
    --    here you can declare all the variables that you need - just once - use them later to fetch into
    c_ID          Number(6) := 0;
    c_NAME        VarChar2(32) := '';
    c_BORN        DATE;
    --
--  ---------------------------------------------------------------------------------------------------
  PROCEDURE Init   AS
      BEGIN
          -- here you can define the part that doesn't change of your pages long SQL and embed some variables for parts that does change
          m_sql := m_select || ' FROM (SELECT 1 "ID", ''John'' "NAME", To_Date(''1987-NOV-27'', ''yyyy-MON-dd'') "BORN" From dual Union All
                                       SELECT 2 "ID", ''Mary'' "NAME", To_Date(''1989-MAY-29'', ''yyyy-MON-dd'') "BORN" From dual Union All
                                       SELECT 3 "ID", ''Mike'' "NAME", To_Date(''1991-JAN-20'', ''yyyy-MON-dd'') "BORN" From dual 
                                      ) base' || ' ' || m_from || ' ' || m_where;
          --
      END Init;
--    -----------------------------------------------------------------------------------------------------------
  FUNCTION Get_Cursor (p_add_select IN VARCHAR2 := 'Select base.* ', p_add_from IN VARCHAR2 := '', p_add_where IN VARCHAR2 := '') RETURN SYS_REFCURSOR AS 
      BEGIN
          Declare
              m_cursor      SYS_REFCURSOR;
          Begin
              --  building cursors 
              m_select := p_add_select;
              m_from := p_add_from;
              m_where := p_add_where;
              Init;
              --  return built cursor
              OPEN m_cursor FOR m_sql;   
              RETURN m_cursor;
          End;
      END Get_Cursor;
--    ------------------------------------------------------------------------------------------------------------
  PROCEDURE Do_It  AS
      BEGIN
          Declare
              l_cursor   SYS_REFCURSOR;
          Begin
              For i In 0..3 Loop 
                  If i = 0 Then
                        l_cursor := Get_Cursor();
                  Else
                        l_cursor := Get_Cursor(p_add_where => ' WHERE ID = ' || i);
                  End If;
      DBMS_OUTPUT.PUT_LINE(m_sql || Chr(10));       -- test print out
                  LOOP 
                      FETCH l_cursor Into c_ID, c_NAME, c_BORN;
                      EXIT WHEN l_cursor%NOTFOUND;
      DBMS_OUTPUT.PUT_LINE(c_ID || ' | ' || c_NAME || ' | ' || c_BORN);   -- test print out
                  END LOOP;
                  CLOSE l_cursor;
              End Loop;
              --
              l_cursor := Get_Cursor( p_add_select => 'Select added.* ', 
                                      p_add_from => 'Left Join (Select 4 "ID", ''Bob'' "NAME", To_Date(''06.07.2000'', ''dd.mm.yyyy'') "BORN" From Dual) added ON(added.ID > base.ID)', 
                                      p_add_where => ' WHERE added.ID = 4');
              FETCH l_cursor Into c_ID, c_NAME, c_BORN;
      DBMS_OUTPUT.PUT_LINE('-- ************************************ --');   -- test print out
      DBMS_OUTPUT.PUT_LINE(m_sql);                                          -- test print out
      DBMS_OUTPUT.PUT_LINE(c_ID || ' | ' || c_NAME || ' | ' || c_BORN);     -- test print out
          End;
      END Do_It;
END REF_CURSORS;

测试功能在 Do_It Procedure 中编码,其中将一些循环和条件放在一起,最后嵌入了一些额外的数据,并设置了全新的 select 语句。

调用和结果打印输出:

SET SERVEROUTPUT ON
BEGIN
      REF_CURSORS.Do_It;
END;
/
anonymous block completed
Select base.*  FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual 
                                      ) base  

1 | John | 27-NOV-87
2 | Mary | 29-MAY-89
3 | Mike | 20-JAN-91
Select base.*  FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual 
                                      ) base   WHERE ID = 1

1 | John | 27-NOV-87
Select base.*  FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual 
                                      ) base   WHERE ID = 2

2 | Mary | 29-MAY-89
Select base.*  FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual 
                                      ) base   WHERE ID = 3

3 | Mike | 20-JAN-91
-- ************************************ --
Select added.*  FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
                                       SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual 
                                      ) base Left Join (Select 4 "ID", 'Bob' "NAME", To_Date('06.07.2000', 'dd.mm.yyyy') "BORN" From Dual) added ON(added.ID > base.ID)  WHERE added.ID = 4
4 | Bob | 06-JUL-00

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM