如何在PL/SQL中创建枚举类型?

7
例如,我想制作自己的布尔类型并将其称为Bool。我该如何做?
或者,一个交通信号灯类型,即只有红色、黄色、绿色(当然还有null)的类型。
5个回答

11

我不认为由 A.B.Cade 提供的解决方案完全正确。假设我们有以下过程:

procedure TestEnum(enum_in lights);

enum_in 的值是什么?红色? 黄色? 绿色?

我提出另一种解决方案。这里是一个示例包。

CREATE OR REPLACE PACKAGE pkg_test_enum IS

  SUBTYPE TLight IS BINARY_INTEGER RANGE 0..2;
  Red CONSTANT TLight := 0;
  Yellow CONSTANT TLight := 1;
  Green CONSTANT TLight := 2;

  --get sting name for my "enum" type
  FUNCTION GetLightValueName(enum_in TLight) RETURN VARCHAR2;

  PROCEDURE EnumTest(enum_in TLight);

END pkg_test_enum;


CREATE OR REPLACE PACKAGE BODY pkg_test_enum IS 
  FUNCTION GetLightValueName(enum_in TLight) 
  RETURN VARCHAR2
  IS
    ResultValue VARCHAR2(6);
  BEGIN
    CASE enum_in 
      WHEN Red THEN ResultValue := 'Red';
      WHEN Green THEN ResultValue := 'Green';
      WHEN Yellow THEN ResultValue := 'Yellow';
      ELSE ResultValue := '';
    END CASE;
    RETURN ResultValue;
  END GetLightValueName;


  PROCEDURE EnumTest(enum_in TLight)
  IS
  BEGIN
    --do stuff
    NULL;
  END EnumTest;
END pkg_test_enum;

我现在可以在不同的包中使用TLight。我现在可以针对预定义值或null测试enum_in

以下是使用示例:

begin
  pkg_test_enum.EnumTest(pkg_test_enum.Red);
end;

另外,您可以将此类型设置为非空。

SUBTYPE TLight 是二进制整数,范围为0..2,并且不能为空;


5

这篇博客介绍了一种使用常量值来完成的方法

除了常量之外,该博客还定义了一种有效颜色的子类型。

SQL> declare
  2  RED constant number(1):=1;
  3  GREEN constant number(1):=2;
  4  BLUE constant number(1):=3;
  5  YELLOW constant number(1):=4;
  6  --
  7  VIOLET constant number(1):=7;
  8  --
  9  subtype colors is binary_integer range 1..4;
 10  --
 11  pv_var colors;
 12  --
 13  function test_a (pv_var1 in colors) return colors
 14  is
 15  begin
 16     if(pv_var1 = YELLOW) then
 17             return(BLUE);
 18     else
 19             return(RED);
 20     end if;
 21  end;
 22  --

这并不是我认真阅读的博客。请查看我的修改后答案中的链接,你会找到如何完成它。 - Klas Lindbäck
1
另一种方法是使用集合。这两个示例的SQLPlus友好源代码也可用。 - user272735
谢谢Zulu。我已经直接在答案中添加了关键部分。 - Klas Lindbäck

5
我能想到的最接近的是:
create or replace type lights as object
(
  red varchar2(8),
  yellow varchar2(8),
  green varchar2(8),
constructor function lights return self as result
)

以及正文:

create or replace type body lights is
constructor function lights return self as result is
begin
  self.red = 'red';
  self.yellow = 'yellow';
  self.green = 'green';
  return;
end;
end;

然后在代码中你可以使用它:

declare
l lights := new lights;
begin
   dbms_output.put_line(l.red);
end;

0

我之前使用了与@mydogtom和@klas-lindbäck相同的方法。当我试图刷新记忆时,我发现了这个方法。然而,@a-b-cade建议的对象方法让我思考。我同意@mydogtom所描述的问题(什么是值?),但是使用对象是否可能呢。

我想出了一种方法,它使用一个具有单个成员属性的对象来表示枚举的值,并为每个可能的值提供静态函数。我无法看到如何与子类型结合使用以对值字段进行真正的限制,也不能正式地使其非空。但是,我们可以在构造函数中验证它。与“正确”的枚举(例如Java中的枚举)相比,功能上的缺点是我们无法阻止某人直接将val属性更新为无效值。但只要人们使用构造函数和set_value函数,就是安全的。我不确定开销(包括运行时创建对象和维护对象等方面)是否值得,因此我可能会继续使用@mydogtom描述的方法,但我不确定。

你也可以将名称作为属性,并在set_value中设置(类似于@a-b-cade的版本),但这会添加另一个可以直接更新的属性,因此会出现另一组状态,其中val和name不匹配,因此我更喜欢使用名称作为函数的方法。
以下是使用我的demo_enum类型的示例用法:
procedure do_stuff(enum in demo_enum) is
begin
   if enum.eqals(demo_enum.foo()) then
      -- do something
   end if;
end do_stuff;

或者

procedure do_stuff(enum1 in demo_enum, enum2 in demo_enum) is
begin
   if enum1.eqals(enum2) then
      -- do something
   end if;
end do_stuff;

我所做的是定义一个基类,尽可能地包含以下内容:实际的val字段、静态值的equals函数、set_valueto_string函数。还有name函数,但这只是被覆盖(无法正式使成员函数抽象,因此基本版本只会抛出异常)。我也使用name作为检查值是否有效的方式,以减少需要枚举可能值的位置数量。
create or replace type enum_base as object(
    -- member field to store actual value
    val integer,

    -- Essentially abstract name function
    -- Should be overridden to return name based on value
    -- Should throw exception for null or invalid values
    member function name return varchar2,

    --
    -- Used to update the value.  Called by constructor
    --
    member procedure set_value(pvalue in integer),
    
    --
    -- Checks the current value is valid
    -- Since we can't stop someone updating the val property directly, you can supply invalid values
    --
    member function isValid return boolean,
    
    --
    -- Checks for equality with integer value 
    -- E.g. with a static function for a possible value:   enum_var.equals( my_enum_type.someval() )
    --    
    member function equals(other in integer) return boolean,
        
    --
    -- For debugging, prints out name and value (to get just name, use name function)
    --      
    member function to_string return varchar2
    
    
) not instantiable not final;
/

create or replace type body enum_base is

    member function name return varchar2 is
    begin
        -- This function must be overriden in child enum classes.
        -- Can't figure out how to do an abstract function, so just throw an error
        raise invalid_number;
    end;
    
    member procedure set_value(pvalue in integer) is
        vName varchar2(3);
    begin
        self.val := pvalue;
        -- call name() in order to also validate that value is valid
        vName := self.name;                          
    end set_value;
    
            
    member function isValid return boolean is
        vName varchar2(3);
    begin
        begin
            -- call name() in order to also validate that value is valid
            vName := self.name;   
            return true; 
        exception
            when others then
                return false;
        end;            
    end isValid;
    
    
    member function equals(other in integer) return boolean is
    begin
        return self.val = other;
    end equals;
    
    member function to_string return varchar2 is
    begin 
        if self.val is null then
            return 'NULL';
        end if;

        return self.name || ' (' || self.val || ')';
    end to_string;
end;
/

在实际的枚举类中,我必须定义一个构造函数(它只是调用set_value),并重写name函数以返回每个可能值的名称。然后,我为每个可能的值定义一个静态函数,该函数返回该值的整数索引。最后,我定义了一个equals的重载,将其与同一类型的另一个枚举进行比较。如果您想要将其他属性附加到每个值,则可以通过定义其他函数来实现。
create or replace type demo_enum under enum_base (
            
    -- Note: the name of the parameter in the constructor MUST be the same as the name of the variable.  
    -- Otherwise a "PLS-00307: too many declarations" error will be thrown when trying to instanciate
    -- the object using this constructor         
    constructor function demo_enum(val in integer) return self as result,
    
    --
    -- Override name function from base to give name for each possible value and throw 
    -- exception for null/invalid values
    --
    overriding member function name return varchar2,
    
    --
    -- Check for equality with another enum object
    --  
    member function equals(other in demo_enum) return boolean,

    
    --
    -- Define a function for each possible value
    --
    static function foo return integer,
    static function bar return integer
        
) instantiable final;
/

create or replace type body demo_enum is
    
    constructor function demo_enum(val in integer) return self as result is        
    begin   
        self.set_value(val);        
        return;     
    end demo_enum;
    
    
    overriding member function name return varchar2 is
    begin
        if self.val is null then
            raise invalid_number;
        end if;
        
        case self.val
            when demo_enum.foo() then
                return 'FOO';
            when demo_enum.bar() then
                return 'BAR';
            else 
                raise case_not_found; 
        end case;
    end;    
        
    member function equals(other in demo_enum) return boolean is
    begin
        return self.val = other.val;
    end equals;
            
    static function foo return integer is
    begin 
        return 0;
    end foo;
    
    static function bar return integer is
    begin 
        return 1;
    end bar;    

end;
/

这可以进行测试。我定义了两组测试。一组是手动测试,用于测试此特定枚举类型,同时说明用法:

--
-- Manual tests of the various functions in the enum
--
declare
    foo demo_enum := demo_enum(demo_enum.foo());
    alsoFoo demo_enum := demo_enum(demo_enum.foo());
    bar demo_enum := demo_enum(demo_enum.bar());
    
    vName varchar2(100);    
    
    procedure assertEquals(a in varchar2, b in varchar2) is
    begin
        if a <> b then
            raise invalid_number;
        end if;
    end assertEquals;
    
    procedure assertEquals(a in boolean, b in boolean) is
    begin
        if a <> b then
            raise invalid_number;
        end if;
    end assertEquals;
    
    procedure test(vName in varchar2, enum in demo_enum, expectFoo in boolean) is
    begin
        dbms_output.put_line('Begin Test of ' || vName);
        if enum.equals(demo_enum.foo()) then
            dbms_output.put_line(vName || ' is foo');
            assertEquals(expectFoo, true);
        else
            dbms_output.put_line(vName || ' is not foo');
            assertEquals(expectFoo, false);
        end if;
        
        if enum.equals(demo_enum.bar()) then
            dbms_output.put_line(vName || ' is bar');
            assertEquals(expectFoo, false);
        else
            dbms_output.put_line(vName || ' is not bar');
            assertEquals(expectFoo, true);
        end if;
                
        if enum.equals(foo) then
            dbms_output.put_line(vName || '.equals(vFoo)');
            assertEquals(expectFoo, true);
        else
            assertEquals(expectFoo, false);
        end if;
        
        if expectFoo then
            assertEquals(enum.name, 'FOO');
        else
            assertEquals(enum.name, 'BAR');
        end if;
        
        assertEquals(enum.isValid, true);
        
        case enum.val
            when demo_enum.foo() then
                dbms_output.put_line(vName || ' matches case foo');
            when demo_enum.bar() then
                dbms_output.put_line(vName || ' matches case bar');
            else
                dbms_output.put_line(vName || ' matches no case!!!');
        end case;
        
        dbms_output.put_line(vName || ': ' || enum.to_string());
        dbms_output.put_line('--------------------------------------------------');
        dbms_output.put_line('');
    end test;
begin    
    test('foo', foo, true);
    test('bar', bar, false);
    test('alsoFoo', alsoFoo, true);
        
    
    foo.val := -1;
    assertEquals(foo.isValid, false);    
    begin
        vName := foo.name;    
    exception
        when case_not_found then
            dbms_output.put_line('Correct exception for fetching name when invalid value: ' || sqlerrm);
    end;
    
    foo.val := null;
    assertEquals(foo.isValid, false);    
    begin
        vName := foo.name;    
    exception
        when invalid_number then
            dbms_output.put_line('Correct exception for fetching name when null value: ' || sqlerrm);
    end;
end;

另一个稍微自动化一些,可以用于任何继承enum_base的枚举类型(只要它不添加其他函数——如果有人知道如何仅查找静态函数,请告诉我)。这将检查您是否错误地将相同的整数值定义为多个可能值的静态函数:
--
-- generated test that no two values are equal 
--
declare
    vSql varchar2(4000) := '';
    
    typename constant varchar2(20) := 'demo_enum';
    
    cursor posvals is   
        select procedure_name
        from user_procedures
        where object_name = upper(typename)
          and procedure_name not in (upper(typename), 'EQUALS', 'NAME');

    cursor posvals2 is   
        select procedure_name
        from user_procedures
        where object_name = upper(typename)
          and procedure_name not in (upper(typename), 'EQUALS', 'NAME');


    procedure addline(line in varchar2) is
    begin
        vSql := vSql || line || chr(10);
    end;
begin
    addline('declare');
    addline('   enum ' || typename || ';');
    addline('begin');
        
    for posval in posvals loop    
        addline('   enum := ' || typename || '(' || typename || '.' || posval.procedure_name || '());');    
        for otherval in posvals2 loop
            addline('   if enum.equals(' || typename || '.' || otherval.procedure_name || '()) then');
            if otherval.procedure_name = posval.procedure_name then                 
                addline('      dbms_output.put_line(''' || otherval.procedure_name || ' = ' || posval.procedure_name || ''');');
            else
                addline('      raise_application_error(-20000, ''' || otherval.procedure_name || ' = ' || posval.procedure_name || ''');');
            end if;
            addline('   else');
            if otherval.procedure_name = posval.procedure_name then                 
                addline('      raise_application_error(-20000, ''' || otherval.procedure_name || ' != ' || posval.procedure_name || ''');');
            else
                addline('      dbms_output.put_line(''' || otherval.procedure_name || ' != ' || posval.procedure_name || ''');');
            end if;
            addline('   end if;');
        end loop;        
        addline('');
    end loop;
    addline('end;');
    
    execute immediate vSql;
end;

0
例如,枚举类型包含"是"和"否"的取值。
CREATE OR REPLACE TYPE t_yes_no_enum AS OBJECT
( yes_no NUMBER
, CONSTRUCTOR FUNCTION t_yes_no_enum
  ( yes_no NUMBER
  )
  RETURN SELF AS RESULT
, STATIC FUNCTION yes
  RETURN t_yes_no_enum
, STATIC FUNCTION no
  RETURN t_yes_no_enum
, MAP MEMBER FUNCTION yes_no_map
  RETURN NUMBER
) ;
/
CREATE OR REPLACE NONEDITIONABLE TYPE BODY t_yes_no_enum
AS

CONSTRUCTOR FUNCTION t_yes_no_enum
( yes_no NUMBER
)
RETURN SELF AS RESULT
IS
BEGIN
  self.yes_no := yes_no ;

  IF  utl_call_stack.dynamic_depth > 1
  AND utl_call_stack.unit_type(2) = 'TYPE BODY'
  AND utl_call_stack.concatenate_subprogram(utl_call_stack.subprogram(2)) LIKE 'T_YES_NO_ENUM.%'
  AND utl_call_stack.owner ( 2 ) = utl_call_stack.owner ( 1 )
  THEN
    NULL ;
  ELSE
    raise_application_error ( -20184 , 'Constructor calling prohibited. For object type creation use it''s static factory methods.' ) ;
  END IF ;

  RETURN ;
END ;

STATIC FUNCTION yes
RETURN t_yes_no_enum
IS
BEGIN
  RETURN NEW t_yes_no_enum ( 1 ) ;
END ;

STATIC FUNCTION no
RETURN t_yes_no_enum
IS
BEGIN
  RETURN NEW t_yes_no_enum ( 2 ) ;
END ;

MAP MEMBER FUNCTION yes_no_map
RETURN NUMBER
IS
BEGIN
  IF coalesce ( self.yes_no , -999 ) NOT IN ( 1 , 2 ) THEN
    raise_application_error ( -20185 , 'Changing object type attribute value prohibited.' ) ;
  END IF ;
  RETURN self.yes_no ;
END ;

END ;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接