• Home
  • About
    • Young's Github Pages photo

      一日不作一日不食

    • About
    • Github
  • Posts
    • All Posts
    • All Tags
  • Projects

오라클 DBMS 정리 14

09 Nov 2018

Reading time ~8 minutes

오라클 DBMS 정리 14 - PL/SQL PROCEDURE


CRUD (CREATE, READ, UPDATE, DELETE)

  • CREATE, INSERT
  • SELECT
  • ALTER, GRANT, REVOKE, UPDATE
  • DROP, DELETE, TRUNCATE

PROCEDURE로 CRUD

-- 프로시저 CRUD 연습용 테이블 생성(구조만 복사)
CREATE TABLE test_proc
AS (SELECT empno, ename, hiredate,sal,job
    FROM emp
    WHERE 1=0);
                                   
-- pk 설정
ALTER TABLE test_proc ADD CONSTRAINT pk_test_proc PRIMARY KEY(empno);
          
-- pk 설정 확인
SELECT * FROM user_constraints
WHERE TABLE_NAME='TEST_PROC';
-- 프로시저를 이용한 INSERT
-- 사원번호, 사원명, 연봉, 직급를 입력받아 test_proc테이블에
-- 추가하는 PROCEDURE를 작성하세요.   
-- 제약들                      
--   * 사원번호는 1~9999 사이의 값만 입력되어야 한다.
--   * 연봉 2500~8000 사이만 입력되어야한다.
--     2500 이하라면 2500으로 8000 이상이라면 8000으로 설정하여 추가
--   * 직급은 사원, 주임, 대리, 과장, 차장, 부장, 이사가 입력되었을 때만
--     추가한다.
--   * 사원명은 영어인 경우 첫자를 대문자로 변경하여 추가                                         
--   * 중복된 사원 번호가 입력되면 예외로 처리한다.

CREATE OR REPLACE PROCEDURE insert_test_proc(
    empno NUMBER,
    ename VARCHAR2,
    sal NUMBER,
    job VARCHAR2,
    msg OUT VARCHAR2,
    row_cnt OUT NUMBER
)                       
IS
    -- in parameter는 값 변경이 안되기 때문에 값이 변경되는 경우
    -- 새로운 변수를 생성해야 한다
    temp_sal NUMBER := sal;
    

BEGIN
    row_cnt := 0;

    IF empno BETWEEN 1 AND 9999 THEN
                                
        IF temp_sal < 2500 THEN
            temp_sal := 2500;
        ELSIF temp_sal > 8000 THEN
            temp_sal := 8000;
        END IF;              
        
        IF job IN ('사원', '주임', '대리', '과장', '차장', '부장', '이사') THEN
            
            INSERT INTO test_proc(empno, ename, hiredate, sal, job)
            VALUES(empno, INITCAP(ename), SYSDATE, temp_sal, job);
                   
            row_cnt := SQL%ROWCOUNT;
            -- COMMIT은 커서의 값을 사용 후에 사용(커서값을 날려버림)
            COMMIT;
            msg := empno||'번 사원 정보가 추가되었습니다.';
            
        ELSE
            msg := job||'은 입력가능한 직급이 아닙니다';        
        ENd IF;                   
                
    ELSE
        msg := empno||'사원번호는 1에서 9999사이가 아닙니다.';
    END IF;  
    
    EXCEPTION
    WHEN DUP_VAL_ON_INDEX THEN
        msg := empno||'번 사원번호는 이미 존재합니다.';
  
    -- 이런식이 좋은 예외처리
    WHEN OTHERS THEN                                   
        IF sqlcode = -12899 THEN
            msg := ename||'명은 한글 3자, 영어 10자를 초과하였습니다.';
        END IF;

END;
/
-- insert_test_proc 프로시저 out parameter를 받을 바인드 변수 선언
VAR msg VARCHAR2(300);
VAR cnt NUMBER;

-- 직접 실행
EXEC insert_test_proc(1111,'테스트',3000,'주임',:msg,:cnt);

-- 바인드 변수 출력, 결과 확인
PRINT msg;
PRINT cnt;
-- 프로시저를 이용한 UPDATE
-- 사원번호, 직급, 연봉을 입력받아 test_proc테이블에서 변경하는 프로세저
-- 제약들
--  * 직급은 사원, 주임, 대리, 과장, 차장, 부장, 이사,
--    상무, 전무, 대표이사가 입력 되었을 때만 변경
--  * 연봉이 현재 연봉보다 작다면 현재 연봉보다 5% 인상액으로 변경

CREATE OR REPLACE PROCEDURE update_test_proc(
    i_empno NUMBER,
    i_job VARCHAR2,
    sal    NUMBER,
    res_msg OUT VARCHAR2,
    row_cnt OUT NUMBER    
)
IS
    -- 현재 연봉을 받아오기 위해 만든 변수
    temp_sal NUMBER;

BEGIN                   
    row_cnt := 0;   
    
    IF i_job IN ('사원','주임','대리','과장',
               '차장','부장','이사','상무',
               '전무','대표이사') THEN
        
        SELECT sal
        INTO temp_sal
        FROM test_proc
        WHERE empno = i_empno;
        
        IF sal < temp_sal THEN
            temp_sal := TRUNC(temp_sal*1.05, 0);
        ELSE
            temp_sal := sal;
        END IF;                
        
        UPDATE test_proc
        SET sal = temp_sal, job = i_job
        WHERE empno = i_empno;    
        
        row_cnt := SQL%ROWCOUNT;
                                
        COMMIT;                  
        
        res_msg := i_empno||'번 사원의 정보가 변경되었습니다. 제시연봉 : '||sal||
                        ', 확정연봉 : '||temp_sal;
    
    ELSE
        res_msg := i_job||'은 사내 직무가 아닙니다.';
    END IF;
    
    EXCEPTION
    WHEN NO_DATA_FOUND THEN
        res_msg := i_empno||'번 사원은 존재하지 않습니다.';
    WHEN OTHERS THEN
        res_msg := sqlerrm;

END;
/
-- 바인드 변수(외부 변수) 선언
VAR msg VARCHAR2(100);
VAR row_cnt NUMBER;

-- 프로시저 실행
EXEC update_test_proc(1112,'주임',3000,:msg,:row_cnt);

-- 처리결과(바인드 변수) 출력
PRINT msg;
PRINT row_cnt;
-- 프로시저를 이용한 DELETE                 
-- 언어가 아니라 confirm dialog 없이 삭제처리
-- 사원번호를 입력받아 test_proc 테이블에서 삭제하는 PROCEDURE

CREATE OR REPLACE PROCEDURE delete_test_proc(
    i_empno NUMBER,
    msg OUT VARCHAR2,
    cnt OUT NUMBER
)                 
IS
    res_msg VARCHAR2(15);
BEGIN
    
     DELETE FROM test_proc
     WHERE empno = i_empno;
     
     cnt := SQL%ROWCOUNT;
     IF cnt = 1 THEN
         COMMIT;
         msg := i_empno||'번 사원 퇴사. 행복하세요~~';       
     ELSE                               
         msg := i_empno||'번 사원은 존재하지 않습니다.';    
     END IF;
END;
/
-- 바인드 변수 선언
VAR msg VARCHAR2(100);
VAR cnt NUMBER;

-- 프로시저 실행
-- 삭제 : 존재하는 사원번호로 삭제
EXEC delete_test_proc(2222, :msg, :cnt);
               
-- 처리 결과 출력
PRINT msg;
PRINT cnt;

-- 프로시저를 이용한 SELECT
-- 한 조회결과는 내보낼 수 있으나 여러 조회 결과는 REFCURSOR를 사용해야 함
-- 프로시저를 이용한 SELECT
-- 직무(사원, 주임, 대리, 과장, 차장, 부장, 이사, 상무,
-- 전무, 대표이사)일 때 입력받아 해당 직무의
-- 사원수와 연봉합(커서사용)을 out parameter에 설정하는 프로시저

CREATE OR REPLACE PROCEDURE select_test_proc_cnt(
    i_job VARCHAR2,
    cnt_emp OUT NUMBER,
    sum_sal OUT NUMBER,
    msg OUT VARCHAR2
)                     
IS
    temp_sal NUMBER := 0;
    
BEGIN
    sum_sal := 0;
    cnt_emp := 0;      

    IF i_job IN ('사원', '주임', '대리', '과장',
              '차장', '부장', '이사', '상무', '전무', '대표이사') THEN   
  
      -- 커서를 사용해서 여러 조회결과 처리
      FOR rec_emp IN (SELECT sal
                      FROM test_proc
                      WHERE job = i_job)
      LOOP
      -- 조회가 실행될 때마다 사원수 증가
        cnt_emp := cnt_emp + 1;
        sum_sal := sum_sal + rec_emp.sal;
      END LOOP;                              
      
/* 
      -- 이럴 땐 커서 사용 안하고 
      -- COUNT(), SUM() 사용가능
      SELECT COUNT(empno), SUM(sal)
      INTO sum_sal, cnt_emp
      FROM test_proc
      WHERE job=i_job;
*/           
    IF cnt_emp != 0 THEN
      msg := i_job||' 으로 조회된 결과 있음';        
    ELSE
      msg := i_job||' 직급의 사원은 존재하지 않습니다.';
      -- cnt_emp = 0이면 결과가 NULL이 나오기 때문에 할당
      cnt_emp := 0;            
    END IF;                                         
  ELSE
    msg := i_job||'은 없는 직무입니다.';
  END IF;
END;
/
-- 직급에 따른 사원 수, 연봉합 조회
-- out parameter 담을 바인드변수 선언
VAR cnt_emp NUMBER;
VAR sum_sal NUMBER;
VAR msg VARCHAR2(100);

-- 프로시저 실행
EXEC select_test_proc_cnt('주임',:cnt_emp, :sum_sal, :msg);
        
-- 프로시저 결과 출력
PRINT msg;
PRINT cnt_emp;
PRINT sum_sal;

LIKE 검색 시 변수명과 와일드카드 ‘%’를 같이 사용하기

  • WHERE절에서 LIKE ‘%’를 사용한 검색
    • LIKE는 키값 일부를 알때 WHERE절에 사용해 조건 검색을 수행
    • LIKE는 ‘=’보단 속도가 느리다
  • PL/SQL에서 %를 그냥 사용하면 커서명으로 인식함(커서명%속성명)
CURSOR 커서명 IS 
    SELECT zipcode ...
    FROM   zipcode
--    WHERE dong LIKE i_dong% 
--    i_dong이란 이름의 커서로 인식 error!
    WHERE dong LIKE i_dong||'%';  -- 이런식으로 붙여 사용!
-- 변수명과 %를 같이 사용하기
-- 동이름을 입력받아 zipcode테이블의 주소 검색하는 PROCEDURE
-- 우편번호의 오름차순 정렬했을 때 가장 먼저 조회되는
-- 우편번호, 시도, 구군, 동, 번지를 묶어서 out parameter에 할당하고
-- 조회된 우편번호의 건수를 out parameter에 할당할 것            

CREATE OR REPLACE PROCEDURE search_zipcode(
    i_dong VARCHAR2,   
    
    address OUT VARCHAR2,
    search_cnt OUT NUMBER
)
IS                
  -- 1. 커서 선언
    CURSOR cur_zipcode IS
        SELECT   zipcode, sido, gugun,
                 dong, NVL(bunji, '번지없음') bunji
        FROM     zipcode
        WHERE    dong LIKE i_dong||'%'
        ORDER BY zipcode;      
      
    TYPE rec_zip IS RECORD(   
        -- 변수명이 테이블명.컬럼명과 모두 같다면 인식 못하는 버그발생 가능
        -- zipcode zipcode.zipcode%TYPE,
        zip CHAR(7),                     
        -- 위에 선언된 변수가 테이블명과 같다면 테이블을 참조하지 못할 수 있음
        -- zipcode CHAR(7)로 선언 시 밑에 테이블명.속성명이 변수.속성명으로 인식가능.
        sido zipcode.sido%TYPE,
        gugun zipcode.gugun%TYPE,
        dong zipcode.dong%TYPE,
        bunji zipcode.dong%TYPE            
    );                       
    
    -- 인출한 데이터를 저장할 레코드 변수
    zip_data rec_zip;                    
    
    -- 가장 첫 레코드를 저장한 RECORD 변수
    first_data rec_zip;                     
    
    -- 가장 첫 레코드를 알기위한 인덱스 변수
    cnt NUMBER := 0;

BEGIN
             
  -- 2. 커서 열기
  IF cur_zipcode%ISOPEN THEN
        CLOSE cur_zipcode;  
  END IF;             
  OPEN cur_zipcode;
  
  -- 3. 인출
  LOOP
        FETCH cur_zipcode INTO zip_data;
        EXIT WHEN cur_zipcode%NOTFOUND;
        
        cnt := cnt + 1;
        
        IF cnt = 1 THEN
            first_data := zip_data;
        END IF;
  END LOOP;
  
  search_cnt := cur_zipcode%ROWCOUNT;
    
  IF search_cnt != 0 THEN
      address := first_data.zip||' '||first_data.sido||' '||
            first_data.gugun||' '||first_data.dong||' '||first_data.bunji;
  ELSE
      address := i_dong||'은 존재하지 않습니다.';     
  END IF;                        
  
  -- 4. 커서 닫기
  CLOSE cur_zipcode  

  EXCEPTION
  WHEN invalid_cursor THEN
      address := '커서가 잘못된 연산을 수행';
  WHEN others THEN
      address := sqlerrm;

END;
/
-- 동이름으로 우편번호 검색
VAR address VARCHAR2(100);
VAR search_num NUMBER;

EXEC search_zipcode('상도동',:address,:search_num);

PRINT address;
PRINT search_num;

-- 해당 쿼리의 첫번째 레코드와 결과가 같으면 됨
SELECT *
FROM zipcode
WHERE dong LIKE '상도동%'
ORDER BY zipcode;

PROCEDURE에서 여러행이 조회되고 반환할 때

  • SYS_REFCURSOR 사용
  • Procedure 안에서는 조회쿼리문은 생성하되 반복문으로 인출하지 않는다.
CREATE OR REPLACE PROCEDURE 프로시저명(
    커서명 OUT SYS_REFCURSOR
)
IS 

BEGIN
    OPEN 커서명 FOR 쿼리문정의;
    -- 프로시저안에서 절대 인출(FETCH)하면 안됨!
    -- 인출시 밖에서 커서를 받아갈 수 없어짐!
END;
/
  • 호출
    1. BIND변수(커서) 생성 : 커서를 받을 REFCURSOR타입 사용
    2. 프로시저 실행
    3. 결과 출력 : PRINT로 출력하면 커서가 가리키는 모든결과 출력
VAR 커서명 REFCURSOR;    -- 1

EXEC 프로시저명(:REF커서명);  -- 2

PRINT 커서명;            -- 3
-- 프로시저를 이용한 SELECT(다중행조회)
-- 조회된 결과가 여러개라면 out parameter에 담을 수 없다.
-- 바인드변수로 테이블과 레코드타입은 지원안함
-- 커서의 제어권을 밖으로 내주면 됨 = SYS_REFCURSOR를 사용

-- dept 테이블에서 모든 부서정보를 조회하여
-- out parameter로 설정하는  프로시저를 작성

CREATE OR REPLACE PROCEDURE select_all_dept(
    cur_dept OUT SYS_REFCURSOR
)
IS
BEGIN

    OPEN cur_dept
        FOR SELECT deptno, dname, loc
              FROM dept;

END;
/
-- 1. 바인드변수(커서) 선언, SYS_REFCURSOR 사용
VAR cur_dept REFCURSOR;

-- 2. 프로시저 실행
EXEC select_all_dept(:cur_dept);

-- 3. 결과 출력
PRINT cur_dept;

14-01

-- 위 select_all_dept 프로시저에서 내부에 FETCH로 커서사용
...
IS  
    dept_data dept%ROWTYPE;
BEGIN
...
    LOOP
        FETCH cur_dept INTO dept_data;
        EXIT WHEN cur_dept%NOTFOUND;
    
    END LOOP;
...
-- 만약 프로시저 내부에서 FETCH 후 외부에서 커서를 출력하면
-- 당연히 바인드변수를 출력해도 결과가 안나온다(No Records)
EXEC select_all_dept(:cur_dept);
PRINT cur_dept;
-- SYS_REFCURSOR를 이용한 프로시저결과 다중행 조회
-- 부서번호를 입력받아 emp테이블에서 부서별 사원정보를 조회하여
-- out parameter로 저장하는 프로시저 작성
-- * 사원번호, 사원명, 연봉, 부서번호, 부서명, 위치를 조회
-- * 입력값이 10~19번 입력되면 10번부서 조회
--           20~29번 입력되면 20번부서 조회
--           그외는 30번 부서를 조회

CREATE OR REPLACE PROCEDURE select_emp(
    deptno NUMBER,
    cur_join OUT SYS_REFCURSOR
)
IS
    temp_deptno NUMBER := deptno;
BEGIN

    -- 입력되는 부서번호를 10또는 20으로 생성
    -- 10으로 나눈 몫*10으로 원하는 부서별 구함                                   
    temp_deptno := TRUNC(temp_deptno/10, 0)*10;

    IF temp_deptno NOT IN (10, 20) THEN
        temp_deptno := 30;
    END IF;      
    
    OPEN cur_join
        FOR SELECT e.empno, e.ename, e.sal, d.deptno, d.dname, d.loc
            FROM emp e, dept d
            WHERE (e.deptno = d.deptno)
              AND d.deptno = temp_deptno;
END;
/
-- 부서번호 입력하여 부서별 사원정보 조회
VAR cur_emp REFCURSOR;

-- 부서번호 : 10~19 : 10, 20~29 : 20, 그외 : 30
EXEC select_emp(14,:cur_emp);

PRINT cur_emp;

14-02

  • 이런 SYS_REFCURSOR를 사용하는 방식으로 java에서 사용

숙제풀이1

숙제풀이2



오라클DBMS