Oracle 60일차

[FUNCTION]

input과 output 존재(input은 생략될 수 있음)
input과 output의 관계를 정의하는 객체

select sysdate from dual;  -- input value가 없는 함수 sysdate
select substr('abcde', 2) from dual ;  -- 세번째 인수 생략 (default = -1)

** 문법

CREATE [OR REPLACE] FUNCTION 함수명
(입력변수1  데이터타입 := 값,
 입력변수2  데이터타입 := 값,
...)
RETURN 데이터타입
IS|AS
    내부변수1  데이터타입 := 값;
    내부변수2  데이터타입 := 값;
BEGIN
    ...
    RETURN 변수명
EXCEPTION
END;
/

** 예제1) emp 테이블을 사용하여 부서 번호를 입력받으면 해당 부서의 최대 급여를 return하는 함수

CREATE OR REPLACE FUNCTION max_sal
(vdeptno    number
)
RETURN number
IS
    vmaxsal number;
BEGIN
    select max(sal) into vmaxsal
    from scott.emp
    where deptno = vdeptno;

    RETURN vmaxsal;
END;
/

-- 조회
select max_sal(10) from dual;

** 예제1-2) 위에서 만든 함수를 사용하여 각 부서별 최대 급여자의 이름, 부서번호, 급여 출력.

select ename, deptno, sal, max_sal(deptno)
from scott.emp
where sal = max_sal(deptno);
-- 한 행이라서 where에서 비교하는 게 가능하다.

** 예제2) student, exam_01, hakjum 테이블을 사용하여 학번을 입력받고 해당 학생의 학점을 return하는 함수 생성

CREATE OR REPLACE FUNCTION studno_hakjum
(vstudno    number
)
RETURN varchar2
IS
    vgrade  varchar2(10);
BEGIN
    select h.grade into vgrade
    from scott.student s, scott.exam_01 e, scott.hakjum h
    where e.total between h.min_point and h.max_point
    and s.studno = e.studno
    and s.studno = vstudno;

    RETURN vgrade;
END;
/

-- 조회
select studno_hakjum(9412) from dual;
select name, studno_hakjum(studno) as 학점
from scott.student;

[함수 응용]

** 채번 방법

1. 시퀀스
select seq1.nextval from dual;
insert into table1 values(seq1.nextval, …);

2. 테이블 채번 : 테이블 실제 최대값 조회 후 최대값 + 1을 리턴하는 방식
select next_key(스키마명, 테이블명, 컬럼명) from dual;

CREATE OR REPLACE FUNCTION next_key
(vschema    varchar2,
 vtable     varchar2,
 vcolumn    varchar2
)
RETURN number
IS
    vsql    varchar2(100);
    vmax    number;
BEGIN
    vsql := 'select max('||vcolumn||') + 1 from '||vschema||'.'||vtable;
    execute immediate vsql into vmax;
    
    RETURN vmax;
END;
/

-- 조회
select next_key('scott','emp','empno') from dual;

** long -> varchar2 변환

long -> varchar2 변환의 이유 : CTAS 안됨. dblink로 조회도 불가능함. 나중에 현업가서도 아래 코드에서 vsql부분 잘 수정해서 써먹어야됨.

1) long type data 출력
2) 출력된 결과를 직접 varchar2로 변환
varchar2는 원래 4000byte의 사이즈까지만 표현할 수 있지만 PLSQL 블럭에서의 varchar2는 32767byte까지 표현이 가능하다.

-- long type data의 예)
select data_default  -- long type
from dba_tab_columns
where table_name = 'EMP'
and column_name = 'HIREDATE';

create table t1234
as
select table_name, column_name, data_default
from dba_tab_columns
where table_name = 'EMP';
-- error, ORA-00997: LONG 데이터 유형은 사용할 수 없습니다
-- 함수 생성) 권한 문제때문에 sys계정에서 실행시키기
create or replace function get_column_default
(tname  varchar2,
 cname  varchar2)
return varchar2
is
    vsql    varchar2(32767);
    vreturn varchar2(32767);
begin
    vsql := 'select data_default from dba_tab_columns where table_name = '''||tname||''' and column_name = '''||cname||'''';
    execute immediate vsql into vreturn;
    
    return vreturn;
end;
/

-- 함수를 사용한 테이블 생성 시도) 권한 문제때문에 sys계정에서 실행시키기
create table t1234
as
select table_name, column_name, get_column_default(table_name, column_name) as data_default
from dba_tab_columns
where table_name = 'EMP';

-- 조회
select * from sys.t1234;

[TRIGGER]

어떤 행위 전/후에 자동으로 수행하는 작업에 대해 정의하는 객체.
예) 접속 후 접속기록(유저명, 접속시간, IP, terminal 등) 정보를 특정 테이블에 저장

** 행위 종류에 따라 분류 : timing 정의 시 필요
1) dml 트리거
2) ddl 트리거
3) database 트리거

** 문법

** 문법
CREATE [OR REPLACE] TRIGGER 트리거명 
BEFORE|AFTER|INSTEAD                         -- 특정 동작 전/후/일치 시에 대한 타이밍 정의
INSERT|UPDATE|DELETE [OF event1 OR 컬럼명 ]    -- DML 트리거 시 
ON {table_name|view_name|SCHEMA|DATABASE}    -- 대상 정의
[REFERENCING OLD AS old | NEW AS new]        -- 변경 전/후 값에 대한 참조 필요 시
[FOR EACH ROW ]                              -- 행 수준의 트리거 작성 시 사용
[WHEN ( condition ) ]                        -- 트리거 수행 조건 전달 시 사용   
trigger_body
;

log on 트리거

사용자 접속 기록을 남기는 트리거

** 사전 준비

-- step1) 접속 기록을 저장할 테이블 생성
create table log_on_off
(userid     varchar2(50),
 sessionid  number,
 ipaddr     varchar2(15),
 terminal   varchar2(50),
 host       varchar2(50),
 module     varchar2(50),
 stime      timestamp,
 etime      timestamp,
 status     varchar2(50)
);

-- step2) 세션 정보 확인
select sys_context('USERENV','ip_address'),
       sys_context('USERENV','sessionid'),
       sys_context('USERENV','terminal'),
       sys_context('USERENV','host'),
       sys_context('USERENV','module'),
       sys_context('USERENV','session_user')
from dual;

** 트리거 생성

create or replace trigger t_logon
after logon on database
begin
    insert into log_on_off(userid, sessionid, ip_addr, terminal, host, module, stime, status)
    values(user,
           sys_context('USERENV','sessionid'),
           sys_context('USERENV','ip_address'),
           sys_context('USERENV','terminal'),
           sys_context('USERENV','host'),
           sys_context('USERENV','module'),
           systimestamp,
           'SUCCESS');
    commit;
end;
/

-- 조회
select * from log_on_off;

log off 트리거

-- log off 트리거 생성)
create or replace trigger t_logoff
before logoff on database
begin
    update log_on_off
    set etime = systimestamp
    where sessionid = sys_context('USERENV','sessionid');
    commit;
end;
/

** 참고 : orange의 경우 oracle 접속과정에 OCISessionEnd를 사용하지 않고 직접적으로 OCIserverdetach를 호출하는 식으로 세션 관리를 하기 때문에 log off 트리거가 동작하지 않을 수 있다.

[접속 제한 트리거]

목적 : Oracle 접속 시 허가된 유저, 접속 시간 등을 관리할 목적으로 생성.

-- step1. 트리거 생성
create or replace trigger t_logon
after logon on database
begin
    if upper(user) = 'HR' then
        insert into log_on_off(userid, sessionid, ip_addr, terminal, host, module, stime, status)
        values (user, 
            sys_context('USERENV','sessionid'),
            sys_context('USERENV','ip_address'),
            sys_context('USERENV','terminal'),
            sys_context('USERENV','host'),
            sys_context('USERENV','module'),
            systimestamp,
            'FAILED');
        commit;
        raise_application_error(-20005, '허가되지 않은 사용자다!!!!!!!!!!!!');
    else
        insert into log_on_off(userid, sessionid, ip_addr, terminal, host, module, stime, status)
        values (user, 
            sys_context('USERENV','sessionid'),
            sys_context('USERENV','ip_address'),
            sys_context('USERENV','terminal'),
            sys_context('USERENV','host'),
            sys_context('USERENV','module'),
            systimestamp,
            'SUCCESS');
        commit;
    end if;
end;
/

-- step2. hr계정으로 접속 시도(error 발생)

-- step3. 접속 기록 테이블 조회
select * from log_on_off;

-- step4. trigger 삭제
drop trigger t_logon;

select *
from dba_objects
where object_name = 'T_LOGON';

[DML 트리거]

DML 제한(계정, 실행시간), DML 작업 기록을 남기기 위해 주로 사용.

placeholder를 사용하여 값을 전달.
1) :OLD : 이전 값을 나타내는 플레이스홀더
ex) DELETE 시 이전 데이터 저장
: INSERT INTO T_LOG … VALUES(:OLD.EMPNO, :OLD.ENAME, …);

2) :NEW : 새로운 값을 나타내는 플레이스홀더

insert는 :NEW만 존재. delete는 :OLD만 존재.

dml 트리거 내에서는 commit 사용 불가. 최초 수행되는 dml문에 의해 트리거가 작동하기 때문에 하나의 트랜잭션으로 간주, 따라서 최초 수행되는 dml문의 commit과 rollback에 의존.

** 예제)
도서 대여 테이블이 있다고 하자. 도서를 대출할 때 값이 입력되고 반납될 때 반납날짜가 update 되면 자동으로 연체요금이 계산되도록 하는 트리거를 작성하라;
연체요금 : 일당 200원
대여기간 : 3일
– 대출 테이블(대출번호, 고객번호, 도서번호, 대출날짜, 반납날짜)
– 연체 테이블(대출번호, 연체요금)

--1. 테이블 생성
create table rent(
rent_no     number,
gogak_no    number,
book_no     number,
sdate       date,
edate       date);

create table delay(
rent_no     number,
delay_fare  number);

--2. rent 테이블에 데이터 입력 시 자동으로 delay 테이블 insert 트리거 작성 (sqlplus system/oracle에서 수행)
create or replace trigger t_rent
after insert on rent
for each row
    begin
        insert into delay(rent_no) values(:new.rent_no);
    end;
/

--대출 발생 insert 시도)
insert into rent(rent_no, gogak_no, book_no, sdate) values(1000, 1, 100000, sysdate);
insert into rent(rent_no, gogak_no, book_no, sdate) values(1001, 2, 200000, sysdate+1);
insert into rent(rent_no, gogak_no, book_no, sdate) values(1003, 3, 500000, sysdate);
commit;

select * from rent;
select * from delay;

--3. rent 테이블에 반납날짜가 수정될 때 자동으로 delay 테이블에 연체요금 update 트리거 작성 (sqlplus system/oracle에서 수행)
create or replace trigger t_delay_fare
after update of edate on rent                    -- update column 제한
for each row
    declare
        vfare   number;
    begin
        if (:new.edate - :old.sdate) <= 3 then
            vfare := 0;
        else 
            vfare := trunc(((:new.edate - :old.sdate) - 3) * 200);
        end if;
        
        update delay
           set delay_fare = vfare
         where rent_no = :old.rent_no;
    end;
/

--반납 발생 insert 시도)
select * from rent;

update rent set edate = sysdate + 1 where rent_no = 1000;
commit;

update rent set edate = sysdate + 7 where rent_no = 1001;
commit;

update rent set edate = sysdate + 20 where rent_no = 1003;
commit;

select * from rent;
select * from delay;

[PACKAGE]

PLSQL 블럭(프로시저, 함수)의 묶음.
패키지 선언부, 패키지 body부로 구성

1) 패키지 선언부 : 패키지 내 함수나 프로시저 명, 변수 정의
2) 패키지 body부 : 함수나 프로시저의 plsql 블럭 정의

** 문법
1) 패키지 선언부

create [or replace] package 패키지명
is|as
    변수 정의;
    프로시저나 함수 정의;
end 패키지명;
/

2) 패키지 body부

create [or replace] package body 패키지명
is|as
    변수 정의;
    프로시저나 함수 plsql 블럭 정의;
end 패키지명;
/

입력변수가 없는 여러 프로시저를 갖는 패키지 생성

** 예제1) emp 테이블에서 emp_sum(급여총합) 프로시저, emp_avg(급여평균) 프로시저를 갖는 emp_total 패키지 생성

--step1) 패키지 선언부 생성
create or replace package emp_total
is
    procedure emp_sum;
    procedure emp_avg;
end emp_total;
/

--step2) 패키지 body부 생성
create or replace package body emp_total
as
procedure emp_sum
is
  vcount    number;
  vsum      number;
begin
  select count(*), sum(sal) into vcount, vsum
    from scott.emp;
  dbms_output.put_line('인원수: '||vcount||'명, 급여총합: '||vsum||'입니다.');
end emp_sum;
procedure emp_avg
is
  vcount    number;
  vavg      number;
begin
  select count(*), round(avg(sal)) into vcount, vavg
    from scott.emp;
  dbms_output.put_line('인원수: '||vcount||'명, 급여평균: '||vavg||'입니다.');
end emp_avg;
end emp_total;
/

--실행(server output tab에서 실행)
exec emp_total.emp_sum;
exec emp_total.emp_avg;

Leave a Comment