Notice
Recent Posts
Recent Comments
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Archives
Today
Total
관리 메뉴

SYDev

[이것이 자바다] Chapter 20. 데이터베이스 입출력 본문

Programming Lang/Java

[이것이 자바다] Chapter 20. 데이터베이스 입출력

시데브 2025. 2. 10. 15:35

1. JDBC 개요

  • JDBC(Java DataBase Connectivity): 데이터베이스와 연결해서 데이터 입출력 작업을 할 수 있도록 자바에서 제공하는 라이브러리
  • DBMS의 종류와 상관없이 동일하게 사용할 수 있는 클래스와 인터페이스로 구성
  • JDBC 인터페이스를 통해 실제로 DB와 작업하는 것은 JDBC Driver, JDBC Driver는 JDBC 인터페이스를 구현한 것

  • DriverManager: JDBC Driver를 관리하며, DB와 연결해서 Connection 구현 객체를 생성
  • Connection: connection 인터페이스는 Statement, PreparedStatement, CallableStatement 구현 객체를 생성하며, 트랜잭션 처리DB 연결을 끊을 때 사용
  • Statement: statement 인터페이스는 SQL의 DDL(Data Definition Language)과 DML(Data Manipulation Language)을 실행할 때 사용, 주로 변경되지 않는 정적 SQL문을 실행할 때 사용
  • PreparedStatement: SQL의 DDL, DML문을 실행할 때 사용, Statement와 차이점은 매개변수화된 SQL문을 사용할 수 있기 때문에 편리성과 보안이 좋음 -> 따라서 주로 statement보다 많이 사용
  • CallableStatement: DB에 저장되어 있는 프로시저(procuder)와 함수(function)를 호출할 때 사용
  • ResultSet: DB에서 가져온 데이터를 읽을 때 사용

 

2. DBMS 설치

https://velog.io/@devsaza/M1-M2-Mac-OS%EC%97%90%EC%84%9C-Oracle-DB-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0

 

M1, M2 Mac OS에서 Oracle DataBase 사용하기

M1 이상의 CPU를 사용하는 Mac에서 Docker를 이용해 Oracle Database를 설치해봅시다.

velog.io

2.1. Colima 설치

  • Colima: 무거운 Docker Desktop을 대신해, CLI 환경에서 Docker containers를 실행하는 open source software
brew install colima

2.2. Colima 실행

  • x86_64 환경에서 colima 실행
colima start --memory 4 --arch x86_64

-> 가상 환경 준비 완료

2.3. Docker Container 실행

  • --restart: unless-stopped -> 재부팅 시에도 자동으로 Oracle DB 실행
  • --name: 컨테이너를 지칭하는 이름 부여하는 옵션, 컨테이너 ID 대신 사용 가능
  • -e: 컨테이너의 환경 변수 설정, ORACLE_PASSWORD라는 환경변수에 비밀번호 할당
  • -p: 포트 배포 및 바인딩
  • -d: 컨테이너를 백그라운드에서 실행
  • gvenzl/oracle-xe 이미지 식별자 -> 오라클 이미지 다운로드
docker run \
 --restart unless-stopped \
 --name oracle2 \
 -e ORACLE_PASSWORD=pass \
 -p 1521:1521 \
 -d \
 gvenzl/oracle-xe

3. Database 연결

  • SQL Developer를 통해 데이터베이스 연결
  • Username: system, Password: pass

4. DB 구성

  • users, boards table 삽입
  • boards 테이블의 bno 값을 제공하는 시퀀스 생성
    • Sequence: 유일한 값을 생성해주는 오라클 객체
    • 시퀀스를 생성하면 기본키와 같이 순차적으로 증가하는 컬럼을 자동적으로 생성할 수 있음
    • 보통 PRIMARY KEY 값을 생성하기 위해 사용
    • 메모리에 Cache 되었을 때, Sequence 값의 액세스 효율 증가
    • SEQUENCE는 테이블과 독립적으로 저장되고 생성, 하나의 Sequence를 여러 테이블에서 사용 가능
  • 계좌 정보가 저장될 accounts 테이블 생성
  • user_create 프로시저 생성
    • 데이터베이스에 대한 일련의 작업을 정리한 절차를 DBMS에 저장한 것 - 영구저장모듈(Persistent Storage Module)이라고도 불림
    • 일련의 쿼리를 하나의 함수처럼 실행하기 위한 쿼리의 집합
  • user_login 함수 생성
    • function: 하나의 특별한 목적의 작업을 수행하기 위해 독립적으로 설계된 코드의 집합
함수와 프로시저의 차이
procedure: 어떤 프로세스를 절차적으로 기술해놓은 것
ex) 쇼핑몰 회원 로그인 -> 구매할 신발 선택 -> 개인정보 및 배송지 선택 -> 결제
function: 각 프로세스를 수행하기 위해 필요한 기능
ex) 쇼핑몰의 로그인 기능 중 ID와 PW를 체크하는 기능
# users, boards 테이블 생성
create table users (
	userid          varchar2(50)	primary key, 
	username		    varchar2(50)	not null,
	userpassword	  varchar2(50)	not null,
	userage			    number(3)		  not null,
	useremail		    varchar2(50)	not null
);

create table boards (
    bno             number          primary key,
    btitle          varchar2(100)   not null,
    bcontent        clob            not null,
    bwriter         varchar2(50)    not null,
    bdate           date            default sysdate,
    bfilename       varchar2(50)    null,
    bfiledata		blob			null
);

# BNO 시퀀스 생성
CREATE SEQUENCE SEQ_BNO NOCACHE;

# accounts 테이블 생성
create table accounts (
  ano     varchar(20) primary key,
  owner   varchar(20) not null,
  balance number      not null
);

insert into accounts (ano, owner, balance) 
values ('111-111-1111', '하여름', 1000000);

insert into accounts (ano, owner, balance) 
values ('222-222-2222', '한겨울', 0);

commit;

# user_create 프로시저 생성
CREATE OR REPLACE PROCEDURE user_create (
    a_userid        IN  users.userid%TYPE, 
    a_username      IN  users.username%TYPE,
    a_userpassword  IN  users.userpassword%TYPE,
    a_userage       IN  users.userage%TYPE,
    a_useremail     IN  users.useremail%TYPE,
    a_rows          OUT PLS_INTEGER
) 
IS
BEGIN
    INSERT INTO users (userid, username, userpassword, userage, useremail)
    VALUES (a_userid, a_username, a_userpassword,  a_userage, a_useremail);
    a_rows := SQL%ROWCOUNT;
    COMMIT;
END;
/

# user_login 함수 생성
CREATE OR REPLACE FUNCTION user_login (
    a_userid        users.userid%TYPE, 
    a_userpassword  users.userpassword%TYPE
) RETURN PLS_INTEGER
IS
    v_userpassword users.userpassword%TYPE;
    v_result PLS_INTEGER;
BEGIN
    SELECT userpassword INTO v_userpassword
    FROM users
    WHERE userid = a_userid;

    IF v_userpassword = a_userpassword THEN
        RETURN 0;
    ELSE
        RETURN 1;
    END IF;
EXCEPTION
    WHEN NO_DATA_FOUND THEN 
        RETURN 2;
END;
/

5. DB 연결

  • client 프로그램에서 DB와 연결하려면 해당 DBMS의 JDBC Driver가 필요
    • BuiltPath에 OJDBC 추가

  • 또한 연결에 필요한 네 가지 정보 필요
    • DBMS가 설치된 컴퓨터의 IP 주소
    • DBMS가 허용하는 Port 번호
    • DB 계정 및 비밀번호
    • 사용하고자 하는 DB 이름

  • 클라이언트 프로그램을 DB와 연결하기 위해, JDBC 프로그램을 메모리로 로딩
    • Class.forName(): 문자열로 주어진 JDBC Driver 클래스를 BuildPath에서 찾고, 메모리로 로딩
    • 이 과정에서 JDBC Driver 클래스의 static 블록이 실행되면서 DriverManager에 JDBC Driver 객체를 등록
    • Build Path에서 클래스를 찾지 못하면 ClassNotFoundException이 발생하므로, 예외 처리를 해야 함
Class.forName("oracle.jdbc.OracleDriver");
  • DriverManager에 JDBC Driver가 등록되면getConnection() method로 DB와 연결 가능
    • 연결 문자열: DBMS마다 다른 형식을 가짐
    • getConnection: Connection 객체를 리턴, 만약 연결이 실패하면 SQLExcpetion이 발생, 예외 처리를 해야 함
Connection conn = DriverManager.getConnection("연결 문자열", "사용자", "비밀번호");

Oracle의 연결 문자열

- 예제

package ch20.oracle.sec5;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionExample {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			// JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
                        // 연결 문자열, 사용자, 비밀번호
			conn = DriverManager.getConnection(
				"jdbc:oracle:thin:@localhost:1521/xe",
				"system",
				"pass"
			);
			
			System.out.println("연결 성공");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 연결 끊기
					conn.close();
					System.out.println("연결 끊기");
				} catch (SQLException e) {}
			}
		}
	}
}

6. 데이터 저장

INSERT INTO users (userid, username, userpassword, userage, useremail)
VALUES (?, ?, ?, ?, ?)

 

위 쿼리문을 Java에서 사용하기 위해 다음 형태로 변경

String sql = new StringBuilder()
    .append("INSERT INTO users (userid, username, userpassword, userage, useremail) ")
    .append("VALUES (?, ?, ?, ?, ?)")
    .toString();

String sql = "" +
    "INSERT INTO users (userid, username, userpassword, userage, useremail) " +
    "VALUES (?, ?, ?, ?, ?)";
  • 매개변수화된 SQL문을 실행하려면 PreparedStatement가 필요 -> prepareStatement() method
  • 이후 ?에 들어갈 값 지정, ?는 순서에 따라 1번부터 번호 부여
PreparedStatement pstmt = conn.prepareStatement(sql);

pstmt.setString(1, "winter");
pstmt.setString(2, "한겨울");
pstmt.setString(3, "12345");
pstmt.setInt(4, 25);
pstmt.setString(5, "winter@mycompany.com");
  • 값을 지정한 이후 executeUpdate() method를 호출 -> SQL문 실행
  • executeUpdate(): 저장된 행 수를 리턴
int rows = pstmt.executeUpdate();

// PreparedStatement 메모리 해제
pstmt.close()

- 예제

package ch20.oracle.sec6;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserInsertExample {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			// JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
			conn = DriverManager.getConnection(
					"jdbc:oracle:thin:@localhost:1521/xe",
					"system",
					"pass"
			);
			
			// 매개변수화된 SQL문 작성
			String sql = "" + 
			    "INSERT INTO users (userid, username, userpassword, userage, useremail) "
				+ "VALUES (?, ?, ?, ?, ?)";
			
			// PreparedStatement 얻기 및 값 지정
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, "winter");
			pstmt.setString(2, "한겨울");
			pstmt.setString(3, "12345");
			pstmt.setInt(4, 25);
			pstmt.setString(5, "winter@mycompany.com");
			
			// SQL문 실행
			int rows = pstmt.executeUpdate();
			System.out.println("지정된 행 수: " + rows);
			
			// PreparedStatement 닫기
			pstmt.close();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 연결 끊기
					conn.close();
				} catch (SQLException e) {}
			}
		}
	}
}

 

  • boards 테이블에 게시물 정보 저장하는 쿼리문
    • SEQ_BNO_NEXTVAL: 시퀀스에서 가져올 번호
    • SYSDATE: 현재 시간
INSERT INTO boards (bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata)
VALUES (SEQ_BNO.NEXTVAL, '눈 오는 날', '함박눈이 내려요', 'winter', SYSDATE, 'snow.jpg', binarydata)

 

위 쿼리문을 Java에서 사용하기 위해 아래와 같이 변경

String sql = "" + 
    "INSERT INTO boards (bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata) " +
    "VALUES (SEQ_BNO.NEXTVAL, ?, ?, ?, SYSDATE, ?, ?)";
     
// 두 번째 매개값은 INSERT문이 실행된 후 가져올 컬럼 값 ( bno 컬럼 값을 가져옴 )
// SQL문이 실행되기 전까지는 SEQ_BNO.NEXTVAL로 얻은 번호를 모르기 때문에 SQL문이 실행된 후에 bno 컬럼에 실제로 저장된 값을 얻는 것
PreparedStatement pstmt = conn.prepareStatement(sql, new String[] {"bno"});

// bfiledata 컬럼은 바이너리 타입(blob)
pstmt.setString(1, "눈 오는 날");
pstmt.setString(2, "함박눈이 내려요");
pstmt.setString(3, "winter");
pstmt.setString(4, "snow.jpg");
pstmt.setBlob(5, new FileInputStream("src/ch20/oracle/sec60/snow.jpg"));
  • INSERT문을 실행하고 저장된 bno 값을 얻는 방법
    • 게시물 정보가 저장되었을 경우(rows가 1일 경우), getGenerateKeys() method로 ResultSet을 얻고, getInt() method로 bno를 얻음
int rows = pstmt.executeUpdate();		// SQL문 실행
if(rows == 1) {
    ResultSet rs = pstmt.getGeneratedKeys();	// new String[] { "bno" }에 기술된 컬럼값을 가져옴

    if(rs.next()) {				// 값이 있다면
        int bno = rs.getInt(1);			// new String[] { "bno" }의 첫 번째 항목 bno 컬럼 값을 읽음
    }
    rs.close();					// ResultSet이 사용했던 메모리 해제
}

- 예제

package ch20.oracle.sec6;

import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class BoardWithFIleInsertExample {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			// JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
			conn = DriverManager.getConnection(
				"jdbc:oracle:thin:@localhost:1521/xe",
				"system",
				"pass"
			);
			
			// 매개변수화된 SQL문 작성
			String sql = "" +
			    "INSERT INTO boards (bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata) " +
			    "VALUES (SEQ_BNO.NEXTVAL, ?, ?, ?, SYSDATE, ?, ?)";
			
			// PreparedStatement 얻기 및 값 지정
			PreparedStatement pstmt = conn.prepareStatement(sql, new String[] { "bno" });
			pstmt.setString(1, "눈 오는 날");
			pstmt.setString(2, "함박눈이 내려요");
			pstmt.setString(3, "winter");
			pstmt.setString(4, "snow.jpg");
			pstmt.setBlob(5, new FileInputStream("src/ch20/oracle/sec6/snow.jpg"));
			
			// SQL문 실행
			int rows = pstmt.executeUpdate();
			System.out.println("저장된 행 수: " + rows);
			
			// bno 값 얻기
			if(rows == 1) {
			    ResultSet rs = pstmt.getGeneratedKeys();	

			    if(rs.next()) {				
			        int bno = rs.getInt(1);	
			        System.out.println("저장된 bno: " + bno);
			    }
			    rs.close();					
			}
			
			// PreparedStatement 닫기
			pstmt.close();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 연결 끊기
					conn.close();
				} catch (SQLException e) {}
			}
		}
	}
}

 

7. 데이터 수정

UPDATE boards SET
    btitle='눈사람',
    bcontent='눈으로 만든 사람',
    bfilename='snowman.jpg',
    bfiledata=binaryData
WHERE bno=1

UPDATE boards SET
    btitle=?,
    bcontent=?,
    bfilename=?,
    bfiledata=?
WHERE bno=?

 

위 쿼리문을 Java에서 사용하기 위해 아래와 같이 변경

String sql = new StringBuilder()
    .append("UPDATE boards SET ")
    .append("btitle=?, ")
    .append("bcontent=?, ")
    .append("bfilename=?, ")
    .append("bfiledata=? ")
    .append("WHERE bno=?")
    .toString();
    
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "눈사람");
pstmt.setString(2, "눈으로 만든 사람");
pstmt.setString(3, "snowman.jpg");
pstmt.setBlob(4, new FileInputStream("src/ch20/oracle/sec7/snowman.jpg"));
pstmt.setInt(5, 3);

int rows = pstmt.executeUpdate();

- 예제

package ch20.oracle.sec7;

import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class BoardUpdateExample {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			// JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
			conn = DriverManager.getConnection(
				"jdbc:oracle:thin:@localhost:1521/xe",
				"system",
				"pass"
			);
			
			// 매개변수화된 SQL문 작성
			String sql = new StringBuilder()
				    .append("UPDATE boards SET ")
				    .append("btitle=?, ")
				    .append("bcontent=?, ")
				    .append("bfilename=?, ")
				    .append("bfiledata=? ")
				    .append("WHERE bno=?")
				    .toString();
				   
			// PreparedStatement 얻기 및 값 지정
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, "눈사람");
			pstmt.setString(2, "눈으로 만든 사람");
			pstmt.setString(3, "snowman.jpg");
			pstmt.setBlob(4, new FileInputStream("src/ch20/oracle/sec7/snowman.jpg"));
			pstmt.setInt(5, 3);

			// SQL문 실행
			int rows = pstmt.executeUpdate();
			System.out.println("수정된 행 수: " + rows);
			
			// PreparedStatement 닫기
			pstmt.close();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 연결 끊기
					conn.close();
				} catch (SQLException e) {}
			}
		}
	}
}

 

8. 데이터 삭제

DELETE FROM boards WHERE bwriter='winter'

DELETE FROM boards WHERE bwriter=?
String sql = "DELETE FROM boards WHERE bwriter=?";

String sql = "DELETE FROM boards WHERE bwriter=?";
PreapredStatement pstmt = conn.prepareStatemet(sql);
pstmt.setString(1, "winter");
int rows = pstmt.executeUpdate();

 

9. 데이터 읽기

  • PreparedStatement를 생성할 때, SQL문이 INSERT, UPDATE, DELETE일 경우에는 executeUpdate() 호출
  • 데이터를 가져오는 SELECT문일 경우에는 executeQuery() method 호출
    • executeQuery(): 가져온 데이터를 ResultSet에 저장하고 리턴
ResultSet rs = pstmt.executeQuery();

9.1. ResultSet 구조

  • ResultSet은 SELECT문에 기술된 컬럼으로 구성된 행(row)의 집합
SELECT userid, username, userage FROM users

 

위 쿼리문의 경우 아래 ResultSet의 내부 구조를 가짐

  • ResultSet은 cursor(행을 가리키는 포인터)가 있는 행의 데이터만 읽을 수 있음
  • ResultSet에는 실제 가져온 데이터 행의 앞과 뒤에 beforeFirst, afterLast 행이 붙음
    • 최초 커서는 beforeFirst를 가리킴
    • next() method를 사용하여 다음 데이터 행으로 이동 가능
    • last 행까지는 true, afterLast 행에서는 false를 리턴
boolean result = rs.next();

 

rs.close();	// ResultSet을 더 이상 사용하지 않느 경우, 메모리 해제

9.2. 데이터 행 읽기

  • 커서가 있는 데이터 행에서 각 컬럼의 값은 Getter method로 읽을 수 있음
  • 컬럼의 데이터 타입에 따라 getXxx() method 사용, 매개값으로 컬럼의 이름 또는 컬럼의 순번을 부여 가능
// 컬럼 이름으로 읽기
String userId = rs.getString("userid");
Stirng userName = rs.getString("username");
int userAge = rs.getInt("userage");

// 컬럼 순번으로 읽기
String userId = rs.getString(1);
String userName = rs.getString(2);
int userAge = rs.getInt(3);

-> SELECT문에 연산식이나 함수 호출이 포함된 경우, 컬럼 순번으로만 읽을 수 있음

9.3. 사용자 정보 읽기

  • Users 테이블에서 한 개의 행(사용자)을 저장할 User 클래스 작성
  • @Data annotation: Getter, Setter, toString() method 자동 생성
package ch20.oracle.sec9.exam1;

import lombok.data;

@Data	// Constructor, Geeter, Setter, hashCode(), equals(), toStirng() 자동 생
public class User {
	private String userId;
	private String userName;
	private String userPassword;
	private int userAge;
	private String userEmail;
}
SELECT userid, username, userpassword, userage, useremail
FROM users
WHERE userid='winter';
String sql = "" + 
    "SELECT userid, username, userpassword, userage, useremail " +
    "FROM users " +
    "WHERE userid=?";

PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "winter");

ResultSet rs = pstmt.executeQuery();
if(rs.next()) {
    User user = new User();
    user.setUserId(rs.getString("userid"));
    user.setUserName(rs.getString("username"));
    user.setUserPassword(rs.getString("userpassword"));
    user.setUserAge(rs.getInt(4));        // 컬럼 순번 이용
    user.setUserEmail(rs.getString(5));   // 컬럼 순번 이용
    System.out.println(user);
} else {
    System.out.println("사용자 아이디가 존재하지 않음");
}

-> System.out.println(user): lombok이 생성한 User의 toString() 메소드를 호출하여 받은 리턴값을 출력

- 예제

package ch20.oracle.sec9.exam1;

import java.sql.ResultSet;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class UserSelectExample {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			// JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
			conn = DriverManager.getConnection(
				"jdbc:oracle:thin:@localhost:1521/xe",
				"system",
				"pass"
			);
			
			// 매개변수화된 SQL문 작성
			String sql = "" + 
				    "SELECT userid, username, userpassword, userage, useremail " +
				    "FROM users " +
				    "WHERE userid=?";

			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, "winter");

			ResultSet rs = pstmt.executeQuery();
			if(rs.next()) {
				User user = new User();
				user.setUserId(rs.getString("userid"));
				user.setUserName(rs.getString("username"));
				user.setUserPassword(rs.getString("userpassword"));
				user.setUserAge(rs.getInt(4));        // 컬럼 순번 이용
				user.setUserEmail(rs.getString(5));   // 컬럼 순번 이용
				System.out.println(user);
			} else {
				System.out.println("사용자 아이디가 존재하지 않음");
			}
			rs.close();
			
			// PreparedStatement 닫기
			pstmt.close();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 연결 끊기
					conn.close();
				} catch (SQLException e) {}
			}
		}
	}
}

9.4. 게시물 정보 읽기

  • 6절의 BoardInsertExample 예제를 이용하여 boards 테이블에 bwriter를 winter로 하는 게시물을 2개 이상 저장
package ch20.oracle.sec9.exam2;

import java.sql.Blob;
import java.util.Date;
import lombok.Data;

@Data
public class Board {
	private int bno;
	private String btitle;
	private String bcontent;
	private String bwriter;
	private Date bdate;
	private String bfilename;
	private Blob bfiledata;
}

 

SELECT bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata
FROM boards
WHERE bwriter='winter';
String sql = "" + 
    "SELECT bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata " +
    "FROM boards " +
    "WHERE bwriter=?";
    
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "winter");
ResultSet rs = pstmt.executeQuery();
while(rs.next()) {
    // 데이터 행을 읽고 Board 객체에 저장
    Board board = new Board();
    board.setBno(rs.getInt("bno"));
    board.setBtitle(rs.getString("btitle"));
    board.setBcontent(rs.getString("bcontent"));
    board.setBwriter(rs.getString("bwriter"));
    board.setBdate(rs.getDate("bdate"));
    board.setBfilename(rs.getString("bfilename"));
    board.setBfiledata(rs.getBlob("bfiledata"));
    
    // 콘솔에 출력
    System.out.println(board);
}

-> next() 메소드가 false를 리턴할 때까지 반복해서 데이터 행을 Board 객체에 저장하고 출력

  • board의 bfiledata는 Blob 객체이므로, 콘솔에 출력하면 oracle.sql.BLOB@5f354bcf와 같이 의미 없는 타입 정보만 출력
    • Blob 객체에 저장된 바이너리 데이터를 얻기 위해서는 다음과 같이 입력 스트림 또는 배열을 얻어내야 함
// InputStream
Blob blob = board.getBfiledata();
InputStream is = blob.getBinaryStream();

// 배열
Blob blob = board.getBfiledata();
byte[] bytes = blob.getBytes(0, blob.length());

// Blobl 객체에서 InputStream을 얻고, 읽은 바이트를 파일로 저장하는 방법
OutputStream os = new FileOutputStream("path" + board.getBfilename());
is.transferTo(os);
os.flush();
os.close();
is.close();

- 예제

package ch20.oracle.sec9.exam2;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class BoardSelectExample {
	public  static void main(String[] args) {
		Connection conn = null;
		try {
			// JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
			conn = DriverManager.getConnection(
				"jdbc:oracle:thin:@localhost:1521/xe",
				"system",
				"pass"
			);
			
			// 매개변수화된 SQL문 작성
			String sql = "" + 
				    "SELECT bno, btitle, bcontent, bwriter, bdate, bfilename, bfiledata " +
				    "FROM boards " +
				    "WHERE bwriter=?";

			// PreparedStatement 얻기 및 값 지정
			PreparedStatement pstmt = conn.prepareStatement(sql);
			pstmt.setString(1, "winter");
			
			// SQL문 실행 후, ResultSet을 통해 데이터 읽기
			ResultSet rs = pstmt.executeQuery();
			while(rs.next()) {
			    // 데이터 행을 읽고 Board 객체에 저장
			    Board board = new Board();
			    board.setBno(rs.getInt("bno"));
			    board.setBtitle(rs.getString("btitle"));
			    board.setBcontent(rs.getString("bcontent"));
			    board.setBwriter(rs.getString("bwriter"));
			    board.setBdate(rs.getDate("bdate"));
			    board.setBfilename(rs.getString("bfilename"));
			    board.setBfiledata(rs.getBlob("bfiledata"));
			    
			    // 콘솔에 출력
			    System.out.println(board);
			    
			    // 파일로 저장
			    Blob blob = board.getBfiledata();
			    if(blob != null) {
			    	InputStream is = blob.getBinaryStream();
			    	OutputStream os = new FileOutputStream(board.getBfilename());
			    	is.transferTo(os);
			    	os.flush();
			    	os.close();
			    	is.close();
			    }
			}
			rs.close();
			
			// PreparedStatement 닫기
			pstmt.close();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 연결 끊기
					conn.close();
				} catch (SQLException e) {}
			}
		}
	}
}

 

10. 프로시저와 함수 호출

  • 프로시저와 함수는 Oracle DB에 저장되는 PL/SQL 프로그램
  • 프로시저 혹은 함수를 호출하면 DB 내부에서 일련의 SQL문을 실행
  • JDBC에서 프로시저, 함수를 호출할 때는 CallableStatement를 사용
    • 프로시저, 함수의 매개변수화된 호출문을 작성하고, Connection의 prepareCall() method로부터 CallableStatement 객체를 얻을 수 있음
    • registerOutParameter(): 리턴값에 해당하는 ?를 지정
    • 그 이외의 ?는 호출 시 필요한 매개값으로 Setter 메소드를 사용해 값을 지정
// 프로시저를 호출하는 경우
String sql = "{ call 프로시저명(?, ?, ...) }";
CallableStatement cstmt = conn.prepareCall(sql);
cstmt.setString(1, "값");
cstmt.setString(2, "값");
cstmt.registerOutParameter(3, 리턴타입);    // 세 번째 ?는 리턴값임을 지정

// 함수를 호출하는 경우
String sql = "{ ? = call 함수명(?, ?, ...) }";
CallableStatement cstmt = conn.prepareCall(sql);
cstmt.registerOutParameter(1, 리턴타입);    // 함수의 경우, 무조건 첫 번째 ?가 리턴값
cstmt.setString(2, "값");
cstmt.setString(3, "값");

// 호출
cstmt.execute();

// 호출 후에는 Getter 메소드로 리턴값 얻을 수 있음
// 프로시저
int result = cstmt.getInt(3);
// 함수
int result = cstmt.getInt(1);

// 더이상 사용하지 않으면 CallableStatement의 메모리 해제
cstmt.close()

-> 함수의 경우 리턴값 자리 명시

10.1. 프로시저 호출

  • 4절에서 생성한 user_create 호출
    • 앞 5개의 IN 매개변수와, 마지막 OUT 매개변수로 구성

 

// 매개변수화된 호출문 작성
String sql = "{call user_create(?, ?, ?, ?, ?, ?)}";
CallableStatement cstmt = conn.prepareCall(sql);

cstmt.setString(1, "summer");
cstmt.setString(2, "한여름");
cstmt.setString(3, "12345");
cstmt.setInt(4, 26);
cstmt.setString(5, "summer@mycompany.com");
cstmt.registerOutParameter(6, Types.INTEGER);

// 프로시저 실행
cstmt.execute();
int rows = cstmt.getInt(6);

- 예제

package ch20.oracle.sec10;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Types;

public class ProcedureCallExample {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			// JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
			conn = DriverManager.getConnection(
				"jdbc:oracle:thin:@localhost:1521/xe",
				"system",
				"pass"
			);
			
			// 매개변수화된 호출문 작성
			String sql = "{call user_create(?, ?, ?, ?, ?, ?)}";
			CallableStatement cstmt = conn.prepareCall(sql);
			
			// ? 값 지정 및 리턴 타입 지
			cstmt.setString(1, "summer2");
			cstmt.setString(2, "한여름");
			cstmt.setString(3, "12345");
			cstmt.setInt(4, 26);
			cstmt.setString(5, "summer@mycompany.com");
			cstmt.registerOutParameter(6, Types.INTEGER);

			// 프로시저 실행
			cstmt.execute();
			int rows = cstmt.getInt(6);
			System.out.println("저장된 행 수: " + rows);
			
			// PreparedStatement 닫기
			cstmt.close();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 연결 끊기
					conn.close();
				} catch (SQLException e) {}
			}
		}
	}
}

10.2. 함수 호출

  • 4절에서 생성한 user_login() 함수 호출
    • 2개의 매개변수와 PLS_INTEGER 리턴 타입으로 구성

String sql = "{? = call user_login(?, ?)}";
CallableStatement cstmt = conn.prepareCall(sql);
cstmt.registerOutParameter(1, Types.INTEGER);
cstmt.setString(2, "winter");
cstmt.setString(3, "12345");

cstmt.execute();
int result = cstmt.getInt(1);	// 첫 번째 ? 값 얻기, 0|1|2 중 하나

- 예제

package ch20.oracle.sec10;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Types;


public class FunctionCallExample {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			//  JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
			conn = DriverManager.getConnection(
					"jdbc:oracle:thin:@localhost:1521/xe",
					"system",
					"pass"
			);
			
			// 매개변수화된 호출문 작성과 CallableStatement 얻기
			String sql = "{? = call user_login(?, ?)}";
			CallableStatement cstmt = conn.prepareCall(sql);
			
			// 값 지정 및 리턴 타입 지정
			cstmt.registerOutParameter(1, Types.INTEGER);
			cstmt.setString(2, "winter");
			cstmt.setString(3, "12345");

			// 함수 실행 및 리턴값 얻기
			cstmt.execute();
			int result = cstmt.getInt(1);	// 첫 번째 ? 값 얻기, 0|1|2 중 하나
			
			// CallableStatement 닫기
			cstmt.close();
			
			// 로그인 결과(Switch Expressions 이용)
			String message = switch(result) {
				case 0 -> "로그인 성공";
				case 1 -> "비밀번호가 틀림";
				default -> "아이디가 존재하지 않음";
			};
			System.out.println(message);
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 연결 끊기
					conn.close();
				} catch (SQLException e) {}
			}
		}
	}
}

 

11. 트랜잭션 처리

  • 트랜잭션(transaction): 기능 처리의 최소 단위
  • DB는 트랜잭션을 처리하기 위해 commit과 rollback을 제공
    • commit: 내부 작업을 모두 성공 처리
    • rollback: 실행 전으로 돌아간다는 의미에서 모두 실패 처리
  • JDBC에서는 INSERT, UPDATE, DELETE 문을 실행할 때마다 자동 커밋 발생
    • 두 가지 UPDATE문을 실행할 때 문제 발생 -> 출금 후 커밋이 발생하면, 입금 작업의 성공 여부와 상관없이 출금 작업만 별도 처리
    • 따라서 JDBC에서 트랜잭션을 코드로 제어하려면, 자동 커밋을 off
    • 자동 커밋 설정 여부는 Connection의 setAutoCommit() method로 가능
// 자동 커밋 종료
conn.setAutoCommit(false);

conn.commit();	// 커밋
conn.rollback(); // 롤백

// 트랜잭션의 일반적 코드 작성 패턴
Connection conn = null;
try {
    // 트랜잭션 시작 ----------------------------
    conn.setAutoCommit(false);
    
    // 소작업 처리
    ...
    // 소작업 처리
    ...
    
    // 커밋 -> 모두 성공 처리
    conn.commit();
    // 트랜잭션 종료 ----------------------------
} catch (Exception e) {
    try {
        // 롤백 -> 모두 실패 처리
        conn.rollback();
    } catch (SQLException e1) {}
} finally {
    if(conn != null) {
        try {
            // 원래대로 자동 커밋 기능 on
            conn.setAutoCommit(true);
            //연결 끊기
            conn.close();
        } catch (SQLException e) {}
    }
}

- 예제

package ch20.oracle;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class TransactionExample {
	public static void main(String[] args) {
		Connection conn = null;
		try {
			// JDBC Driver 등록
			Class.forName("oracle.jdbc.OracleDriver");
			
			// 연결하기
			conn = DriverManager.getConnection(
				"jdbc:oracle:thin:@localhost:1521/xe",
				"system",
				"pass"
			);
			
			// 트랜잭션 시작 --------------------
			// 자동 커밋 기능 끄기
			conn.setAutoCommit(false);
			
			// 출금 작업
			String sql1 = "UPDATE accounts SET balance=balance-? WHERE ano=?";
			PreparedStatement pstmt1 = conn.prepareStatement(sql1);
			pstmt1.setInt(1, 10000);
			pstmt1.setString(2, "111-111-1111");
			int rows1 = pstmt1.executeUpdate();
			if(rows1 == 0) throw new Exception("출금되지 않았음");
			pstmt1.close();
			
			// 출금 작업
			String sql2 = "UPDATE accounts SET balance=balance+? WHERE ano=?";
			PreparedStatement pstmt2 = conn.prepareStatement(sql2);
			pstmt2.setInt(1, 10000);
			pstmt2.setString(2, "222-222-2222");
			int rows2 = pstmt2.executeUpdate();
			if(rows2 == 0) throw new Exception("입금되지 않았음");
			pstmt2.close();
			
			// 수동 커밋 -> 모두 성공 처리
			conn.commit();
			System.out.println("계좌 이체 성공");
			
			// 트랜잭셩 종료 ---------------------
		} catch (Exception e) {
			try {
				// 수동 롤백 -> 모두 실패 처리
				conn.rollback();
			} catch (SQLException e1) {}
			System.out.println("계좌 이체 실패");
			e.printStackTrace();
		} finally {
			if(conn != null) {
				try {
					// 원래대로 자동 커밋 기능 켜기
					conn.setAutoCommit(true);
					// 연결 끊기
					conn.close();
				} catch (SQLException e) {}
			}
		}
	}
}

-> Connection Pool을 사용하는 경우, Connection을 다른 기능 처리를 위해 계속 사용해야 할 가능성이 있으므로, 다시 자동 커밋 기능을 켜둬야 함

커넥션 풀
- 다수의 클라이언트 요청을 처리하는 서버 프로그램은 대부분 Connection Pool 사용
- 커넥션 풀은 일정량의 Connection을 미리 생성시켜놓고, 서버에서 클라이언트의 요청을 처리할 때 Connection 제공하고, 다시 반환받는 역할 수행
- 커넥션 풀을 사용하는 경우, 생성된 Connection을 재사용할 수 있기 때문에 DB 연결 시간을 줄일 수 있고, 전체 Connection 수를 관리할 수 있음

참고자료

 

데이터베이스]SEQUENCE(시퀀스)란 무엇인가?

시퀀스의 정의와 사용 방법에 대해 정리해보려 한다. 개발을 할때 자동으로 키(KEY) 생성을 해야 할 경우 주로 사용한다.? 시퀀스(Sequence)란? - 유일(UNIQUE)한 값을 생성해주는 오라클 객체이다. - 시

pongshowng.tistory.com

 

[Oracle] 함수(Function)와 프로시저(Procedure) 차이

안녕하세요 Foma 💻 입니다. 지난 글들에 프로시저와 함수에 대해 정리했었는데요. 언뜻 보기엔 함수와 프로시저가 거의 똑같다고 생각되더라구요. (함수 글은 여기 에서 프로시저 글을 여기 에

fomaios.tistory.com

 

 

[ Oracle ] 프로시저와 함수의 차이

오라클을 사용하다보면 자주 접하게 되는 프로시저와 함수. ​ 이 둘의 정의와 차이점에 대해 알아보자. 프로시저(Procedure)란? 넓은 의미로는 어떤 업무를 수행하기 위한 절차를 뜻한다. ​ 예를

mjn5027.tistory.com