[비트캠프] 71일차(15주차3일) - JDBC(MyBatis), myapp-42~43(SqlSession, 프록시 패턴)
Mybatis 사용법 : SqlSession 객체
처음엔 아래 5개 메서드만 알면 된다.
sqlSession.
selectOne() → select 문 : 0 또는 1개 결과. 만약 2개 이상의 결과를 리턴하는 select 문을 실행하면 => 예외 발생!
selectList() → select 문 : 0개 이상의 결과
insert()
update()
delete()
→ insert/update/delete 문. 메서드와 상관없이 실행가능! → JDBC API 에서도 executeUpdate() 로 모두 실행!
내부적으로 executeUpdate() 를 call 한다.
Mybatis 사용법 : 메서드와 SQLID
자바코드 : List<Board> list = sqlSession.selectList("BoardMapper.findAll"); 에서 BoardMapper.findAll 을 SQL 아이디라 한다.
BoardMapper → <mapper namespace="BoardMapper"> 이다.
BoardMapper.xml 코드 <select id ="findAll" resultType="bitcamp.myapp.vo.Board"> 에서
Board → bitcamp.myapp.vo.Board 이고
findAll → id="findAll 이다.
Mybatis 사용법 : 컬럼과 프로퍼티
<select id="findAll" resultType="----Board">
select
board_id, → setBoard_id() 호출
title, → setTitle() 호출
created_date, → setCreated_date() 호출
view_cnt → setView_cnt() 호출
↑ 밑줄 친 세터 메서드는 존재하지 않기 때문에 해당 컬럼의 값을 자바 객체에 담지 않는다.
(필드(field), 프로퍼티(property))
해결책!
별명 부여 => board_id as no, → setNo() 호출. 별명과 일치하는 프로퍼티를 찾는다.
Mybatis 사용법 : 결과 레코드와 자바 객체
Board 클래스의 인스턴스를 UML 로 표기하는 방법은 위와 같다.
결과 레코드를 자바 객체로 받는 방법인 selectList() 의 원리이다.
각 결과 레코드의 board_id, title, created_date, view_cnt 를 각 Board 인스턴스로 저장한다.
이들의 주소를 List 에 저장해서 return 한다.
Mybatis 사용법 : in-parameter 와 메서드 파라미터
sqlSession.insert("BoardMapper.insert", b); 에서 b 는 Board 인스턴스이다.
BoardMapper 를 읽고 namespace="BoardMapper" 인 코드를 찾는다.
<insert id="insert" parameterType="bitcamp.myapp.vo.Board"> // Board : 파라미터로 넘어오는 객체의 타입(클래스)
insert into app_board(title, content, pwd)
values(#{title}, #{content}, #{password}) // b.getTitle() 의 리턴값, b.getContent() 의 리턴값, b.getPassword() 의 리턴 값
※ 주의!
In-parameter 는 타입에 상관없이 #{프로퍼티명} 으로 값을 삽입한다. 문자열이라고 해서 '#{...}' 양쪽에 작은 따옴표는 붙여서는 안된다.
Mybatis 사용법 : in-parameter 와 자바 원시타입, String
sqlSession.selectOne("BoardMapper.findByNo", 20); // int 타입
<insert id="insert" parameterType="int"> // "_int" 도 상관없다.
int : Integer
_int : int ← primitive type 인 경우. 예) int 또는 _int 상관 없다.
...
where board_id = #{haha | ok | value} // primitive type 이나 String 타입인 경우 아무 이름을 사용해도 된다.
DAO 와 Transaction
Handler 에서 ① insert() 를 MemberDao 객체에서 한다. 여기서 DBMS 로 insert 한다.
Handler 에서 ② insert() 를 StudentDao 객체에서 한다. ①, ② 를 1개의 트랜잭션으로 묶는다.
insert 가 무조건 개별적으로 실행된다면 insert 후에 commit 하는 것이 맞다.
그런데 다른 변경작업과 묶일 수 있는 경우라면 개별적으로 commit/rollback 해서는 안된다.
결론
→ 여러 DAO 작업을 한 개의 트랜잭션으로 묶어서 사용하는 경우도 있기 때문에 DAO 에서 commit/rollback 하지 말라! → commit/rollback 은 DAO 를 사용하는 측에서 제어해야 한다!
Mybatis 를 사용하는 DAO 의 트랜잭션 제어
① 이전 방식
Handler 객체가 ConnectionFactory 객체에 Connection 요청하면 con1 를 리턴받는다. setAutoCommit(false) - commit(), rollback() 한다.
MemberDao 객체가 ConnectionFactory 객체에 Connection 요청하면 con1 를 리턴 받아 사용한다.
StudentDao 객체가 ConnectionFactory 객체에 Connection 요청하면 con1 를 리턴 받아 사용한다.
Handler 에 Connection 객체를 제어한다. DAO 들은 Handler 가 사용하는 Connection 을 공유한다.
Mybatis를 사용하는 DAO 의 트랜잭션 제어
② 변경 방식
Handler 에서 SqlSession 에 대해
① 트랜잭션 시작
② MemberDao 객체 insert() → SqlSession 객체 사용
③ StudentDao 객체 insert() → SqlSession 객체 사용
→ 같은 트랜잭션으로 묶인 경우 같은 SqlSession 객체를 사용하도록 제어하면 된다.
④ commit()/rollback()
SqlSession 의 트랜잭션 제어
Handler 에서 TransactionManager 객체 생성하고 startTransaction() 실행한다. 여기서 SqlSessionFactory (BitcampSqlSessionFactory) 의 prepareSqlSessionForThread() 실행한다. 그러면 Thread 변수에 SqlSession 기록한다.
MemberDao 에서 txManager의 openSession() 실행한다. SqlSessionFactory 에서 get() 으로 SqlSession 객체 가져오고 스레드에 보관된 SqlSession 리턴한다.
StudentDao 에서 txManager 의 openSession() 실행한다. SqlSessionFactory 에서 get() 으로 SqlSession 객체 가져오고 스레드에 보관된 sqlSession 객체 리턴한다.
Handler 에서 commit()/rollback() 한다.
SqlSessionFactory 클래스의 기능 변경(추가, 변경, 삭제)
→ 기존 클래스의 코드를 손대지 않고 기능 변경 (상속 | Proxy)
사용자가 BitcampSqlSessionFactory 이용해서 요청한다. 이 객체는 《interface》SqlSessionFactory 구현체이고 proxy 이다. MyBatis 구현체에서 +α 하기 위해 작성하였다. 여기서 Mybatis 구현체에 위임한다.
42. Mybatis SQL 매퍼 사용하기(트랜잭션 제어 포함)
### 42. Mybatis SQL 매퍼 사용하기(트랜잭션 제어 포함)
- Mybatis SQL 매퍼를 사용하여 DAO를 구현하는 방법
- Mybatis 를 이용하여 트랜잭션을 제어하는 방법
- Proxy 패턴으로 기존 클래스의 기능을 변경하는 방법
----DaoImpl 생성자에 SqlSessionFactory 객체를 받는다.
try 문에 connection 객체를 얻어서 prepareStatement 를 준비하는 대신, sqlSessionFactory 에서 openSession() 실행해서 SqlSession 객체를 준비한다.
SQL 문을 java 에 직접 작성하지 않고 별로도 작성된 xml 파일을 불러오는 방식을 사용한다.
public class BoardDaoImpl implements BoardDao {
SqlSessionFactory sqlSessionFactory;
public BoardDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public void insert(Board b) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
sqlSession.insert("BoardMapper.insert", b);
}
}
@Override
public List<Board> findAll() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("BoardMapper.findAll");
}
}
@Override
public Board findByNo(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectOne("BoardMapper.findByNo", no);
}
}
@Override
public void increaseViewCount(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
sqlSession.update("BoardMapper.increaseViewCount", no);
}
}
@Override
public List<Board> findByKeyword(String keyword) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("BoardMapper.findByKeyword", keyword);
}
}
@Override
public int update(Board b) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.update("BoardMapper.update", b);
}
}
@Override
public int delete(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.delete("BoardMapper.delete", no);
}
}
}
public class MemberDaoImpl implements MemberDao {
SqlSessionFactory sqlSessionFactory;
public MemberDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public void insert(Member m) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
sqlSession.insert("MemberMapper.insert", m);
}
}
@Override
public List<Member> findAll() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("MemberMapper.findAll");
}
}
@Override
public Member findByNo(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectOne("MemberMapper.findByNo", no);
}
}
@Override
public int update(Member m) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.update("MemberMapper.update", m);
}
}
@Override
public int delete(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.delete("MemberMapper.delete", no);
}
}
}
public class StudentDaoImpl implements StudentDao {
SqlSessionFactory sqlSessionFactory;
public StudentDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public void insert(Student s) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
sqlSession.insert("StudentMapper.insert", s);
}
}
@Override
public List<Student> findAll() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("StudentMapper.findAll");
}
}
@Override
public Student findByNo(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectOne("StudentMapper.findByNo", no);
}
}
@Override
public List<Student> findByKeyword(String keyword) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("StudentMapper.findByKeyword");
}
}
@Override
public int update(Student s) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.update("StudentMapper.update", s);
}
}
@Override
public int delete(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.delete("StudentMapper.delete", no);
}
}
}
public class TeacherDaoImpl implements TeacherDao {
SqlSessionFactory sqlSessionFactory;
public TeacherDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public void insert(Teacher t) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
sqlSession.insert("TeacherMapper.insert", t);
}
}
@Override
public List<Teacher> findAll() {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("TeacherMapper.findAll");
}
}
@Override
public Teacher findByNo(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectOne("TeacherMapper.findByNo", no);
}
}
@Override
public int update(Teacher t) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.update("TeacherMapper.update", t);
}
}
@Override
public int delete(int no) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.delete("TeacherMapper.delete", no);
}
}
}
BoardMapper.xml 파일에 sqlSession 에서 지정한 명령 작성되어 있다.
위에서 insert, update, delete 로 매개변수 전달시 타입을 parameterType=" " 에 지정한다.
SQL 문에서 select 로 가져오는 column 명이 프로퍼티 명과 다르면 as 로 프로퍼티 명 지정한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BoardMapper">
<insert id="insert" parameterType="bitcamp.myapp.vo.Board">
insert into app_board(title, content, pwd)
values(#{title}, #{content}, #{password})
</insert>
<select id="findAll" resultType="bitcamp.myapp.vo.Board">
select
board_id as no,
title,
created_date as createdDate,
view_cnt as viewCount
from
app_board
order by
board_id desc
</select>
<select id="findByNo" parameterType="int" resultType="bitcamp.myapp.vo.Board">
select
board_id as no,
title,
content,
pwd as password,
created_date as createdDate,
view_cnt as viewCount
from
app_board
where
board_id=#{no}
</select>
<update id="increaseViewCount" parameterType="int">
update app_board set
view_cnt = view_cnt + 1
where board_id=#{maumdaerohaedodoi}
</update>
<select id="findByKeyword" parameterType="string" resultType="bitcamp.myapp.vo.Board">
select
board_id as no,
title,
created_date as createdDate,
view_cnt as viewCount
from
app_board
where
title like(concat('%',#{keyword},'%'))
or content like(concat('%',#{keyword},'%'))
order by
board_id desc
</select>
<update id="update" parameterType="bitcamp.myapp.vo.Board">
update app_board set
title=#{title},
content=#{content}
where board_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_board
where board_id=#{no}
</delete>
</mapper>
MemberMapper.xml 은 generatedKey 를 객체에 저장해야 한다. 그래야 그 다음 studentDao.insert(s) 객체의 pk(=fk) 인 member_id 에 memberDao 에서 insert 할 때와 같은 값이 저장된다.
<insert
useGeneratedKeys="true" // 자동 생성된 키를 사용할거면 true 이다.
keyProperty="no" // 객체에 저장할때 프로퍼티 명은 no 이다.
keyColumn="member_id" // DBMS 에 저장된 컬럼 명은 member_id 이다.
>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="MemberMapper">
<insert id="insert" parameterType="bitcamp.myapp.vo.Member"
useGeneratedKeys="true" keyProperty="no" keyColumn="member_id">
insert into app_member(name, email, pwd, tel)
values(#{name}, #{email}, sha2(#{password},256), #{tel})
</insert>
<select id="findAll" resultType="bitcamp.myapp.vo.Member">
select
member_id as no,
name,
email,
created_date as createdDate
from
app_member
order by
member_id desc
</select>
<select id="findByNo" parameterType="int" resultType="bitcamp.myapp.vo.Member">
select
member_id as no,
name,
email,
tel,
created_date as createdDate
from
app_member
where
member_id=#{no}
</select>
<update id="update" parameterType="bitcamp.myapp.vo.Member">
update app_member set
name=#{name},
email=#{email},
pwd=sha2(#{password},256),
tel=#{tel}
where member_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_member
where member_id=#{no}
</delete>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="StudentMapper">
<insert id="insert" parameterType="bitcamp.myapp.vo.Student" >
insert into app_student(
member_id,
pst_no,
bas_addr,
det_addr,
work,
gender,
level)
values(#{no},#{postNo},#{basicAddress},#{detailAddress},#{working},#{gender},#{level})
</insert>
<select id="findAll" resultType="bitcamp.myapp.vo.Student">
select
m.member_id as no,
m.name,
m.email,
m.tel,
s.work as working,
s.level
from
app_student s
inner join app_member m on s.member_id = m.member_id
order by
m.name asc
</select>
<select id="findByNo" parameterType="int" resultType="bitcamp.myapp.vo.Student">
select
m.member_id as no,
m.name,
m.email,
m.tel,
m.created_date as createdDate,
s.pst_no as postNo,
s.bas_addr as basicAddress,
s.det_addr as detailAddress,
s.work as working,
s.gender,
s.level
from
app_student s
inner join app_member m on s.member_id = m.member_id
where
m.member_id=#{no}
</select>
<select id="findByKeyword" parameterType="string" resultType="bitcamp.myapp.vo.Student">
select
m.member_id as no,
m.name,
m.email,
m.tel,
s.work as working,
s.level
from
app_student s
inner join app_member m on s.member_id = m.member_id
where
m.name like(concat('%', #{keyword}, '%'))
or m.email like(concat('%', #{keyword}, '%'))
or m.tel like(concat('%', #{keyword}, '%'))
or s.bas_addr like(concat('%', #{keyword}, '%'))
or s.det_addr like(concat('%', #{keyword}, '%'))
order by
m.name asc
</select>
<update id="update" parameterType="bitcamp.myapp.vo.Student">
update app_student set
pst_no=#{postNo},
bas_addr=#{basicAddress},
det_addr=#{detailAddress},
work=#{working},
gender=#{gender},
level=#{level}
where member_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_student
where member_id=#{no}
</delete>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="TeacherMapper">
<insert id="insert" parameterType="bitcamp.myapp.vo.Teacher" >
insert into app_teacher(
member_id,
degree,
school,
major,
wage)
values(#{no},#{degree},#{school},#{major},#{wage})
</insert>
<select id="findAll" resultType="bitcamp.myapp.vo.Teacher">
select
m.member_id as no,
m.name,
m.tel,
t.degree,
t.major,
t.wage
from
app_teacher t
inner join app_member m on t.member_id = m.member_id
order by
m.name asc
</select>
<select id="findByNo" parameterType="int" resultType="bitcamp.myapp.vo.Teacher">
select
m.member_id as no,
m.name,
m.email,
m.tel,
m.created_date as createdDate,
t.degree,
t.school,
t.major,
t.wage
from
app_teacher t
inner join app_member m on t.member_id = m.member_id
where m.member_id=#{no}
</select>
<update id="update" parameterType="bitcamp.myapp.vo.Teacher">
update app_teacher set
degree=#{degree},
school=#{school},
major=#{major},
wage=#{wage}
where member_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_teacher
where member_id=#{no}
</delete>
</mapper>
mybatis-config.xml 에 driver 정보 및 url, username, password 입력한다.
<mappers> 속성에 Mapper 파일 지정한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="org.mariadb.jdbc.Driver"/>
<property name="url" value="jdbc:mariadb://localhost:3306/studydb"/>
<property name="username" value="study"/>
<property name="password" value="1111"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="bitcamp/myapp/mapper/BoardMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/MemberMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/StudentMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/TeacherMapper.xml"/>
</mappers>
</configuration>
SqlSessionFactoryBuilder 객체로 SqlSessionFactory 객체 만들고 Proxy 패턴 이용하여 BitcampSqlSessionFactory 생성자 매개변수에 전달한다.
이를 TransactionManager 객체 생성시 매개변수로 전달한다.
----DaoImpl 생성시 매개변수로 sqlSessionFactory 전달한다.
public class ServerApp {
/* 생략 */
public ServerApp() throws Exception{
// Mybatis API 사용 준비
// 1) Mybatis 설정 파일 준비
// => resources/bitcamp/myapp/config/mybatis-config.xml
// 2) SQL Mapper 파일 준비
// => resources/bitcamp/myapp/mapper/BoardMapper.xml
// 3) Mybatis 설정 파일을 읽을 때 사용할 입력 스트림 객체 준비
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
"bitcamp/myapp/config/mybatis-config.xml");
// 4) SqlSessionFactoryBuilder 객체 준비
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
// 5) builder를 이용하여 SqlSessionFactory 객체 생성
// 6) 오리지널 SqlSessionFactory에 트랜잭션 보조 기능이 덧붙여진 프록시 객체를 준비한다.
BitcampSqlSessionFactory sqlSessionFactory = new BitcampSqlSessionFactory(
builder.build(mybatisConfigInputStream));
// 7) BitcampSqlSessionFactory객체를 이용하여 트랜잭션을 다루는 객체를 준비한다.
TransactionManager txManager = new TransactionManager(sqlSessionFactory);
BoardDaoImpl boardDao = new BoardDaoImpl(sqlSessionFactory);
MemberDaoImpl memberDao = new MemberDaoImpl(sqlSessionFactory);
StudentDaoImpl studentDao = new StudentDaoImpl(sqlSessionFactory);
TeacherDaoImpl teacherDao = new TeacherDaoImpl(sqlSessionFactory);
this.studentHandler = new StudentHandler("학생", txManager, memberDao, studentDao);
this.teacherHandler = new TeacherHandler("강사", txManager, memberDao, teacherDao);
this.boardHandler = new BoardHandler("게시판", boardDao);
}
TransactionManager 클래스에 startTransaction(), commi(), rollback() 메서드 정의한다.
package bitcamp.util;
import org.apache.ibatis.session.SqlSession;
public class TransactionManager {
BitcampSqlSessionFactory sqlSessionFactory;
public TransactionManager(BitcampSqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
public void startTransaction() {
sqlSessionFactory.prepareSqlSessionForThread();
}
public void commit() {
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.commit();
sqlSessionFactory.clearSqlSessionForThread();
}
public void rollback() {
SqlSession sqlSession = sqlSessionFactory.openSession();
sqlSession.rollback();
sqlSessionFactory.clearSqlSessionForThread();
}
}
BitcampSqlSessionFactory 클래스 정의한다.
생성자 매개변수로 받은 SqlSessionFactory 를 original 에 저장한다.
ThreadLocal 을 생성하고 sqlSessionLocal 에 저장한다.
prepareSqlSessionForThread() 실행시 sqlSessionLocal 에서 sqlSession 을 가져오는데, null 이면 sqlSession 을 새로 생성(autoCommit = false) 해서 sqlSessionLocal 에 저장한다.
clearSqlSessionForThread() 실행시 original 을 close 한다.
openSession() 실행시 sqlSessionLocal 에서 sqlSession 가져오는데, null 이면 트랙잭션 없이 사용할 sqlSession 을 리턴한다.
package bitcamp.util;
import java.sql.Connection;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.TransactionIsolationLevel;
public class BitcampSqlSessionFactory implements SqlSessionFactory {
SqlSessionFactory original;
ThreadLocal<SqlSession> sqlSessionLocal = new ThreadLocal<>();
public void prepareSqlSessionForThread() {
SqlSession sqlSession = sqlSessionLocal.get();
if (sqlSession == null) {
sqlSessionLocal.set(new BitcampSqlSession(original.openSession(false)));
}
}
public void clearSqlSessionForThread() {
BitcampSqlSession sqlSession = (BitcampSqlSession) sqlSessionLocal.get();
sqlSession.closeOriginal();
sqlSessionLocal.set(null);
}
public BitcampSqlSessionFactory(SqlSessionFactory original) {
this.original = original;
}
@Override
public SqlSession openSession() {
SqlSession sqlSession = sqlSessionLocal.get();
if (sqlSession == null) {
// 트랜잭션 없이 사용할 SqlSession 리턴
return original.openSession(true);
}
// 스레드에서 공유할 SqlSession 객체 리턴
return sqlSession;
}
@Override
public SqlSession openSession(boolean autoCommit) {
return original.openSession(autoCommit);
}
@Override
public SqlSession openSession(Connection connection) {
return original.openSession(connection);
}
@Override
public SqlSession openSession(TransactionIsolationLevel level) {
return original.openSession(level);
}
@Override
public SqlSession openSession(ExecutorType execType) {
return original.openSession(execType);
}
@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
return original.openSession(execType, autoCommit);
}
@Override
public SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) {
return original.openSession(execType, level);
}
@Override
public SqlSession openSession(ExecutorType execType, Connection connection) {
return original.openSession(execType, connection);
}
@Override
public Configuration getConfiguration() {
return original.getConfiguration();
}
}
Proxy 패턴 사용하여 트랜잭션에서 예컨대 memberDao.insert(s) 이후 sqlSession 이 자동 close 되지 않도록 BitcampSqlSession 으로 감싸서 close() 본문 주석 처리하고, 추후 closeOriginal() 호출해서 close 되도록 한다.
commit(), rollback() 시 close() 는 자동으로 되는 것으로 추정된다.
Proxy 패턴 사용으로 모든 메서드 호출시 original 메서드에 위임하는 코드가 있다.
package bitcamp.util;
import java.sql.Connection;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.cursor.Cursor;
import org.apache.ibatis.executor.BatchResult;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.session.SqlSession;
public class BitcampSqlSession implements SqlSession {
SqlSession original;
public void closeOriginal() {
original.close();
}
public BitcampSqlSession(SqlSession original) {
this.original = original;
}
@Override
public <T> T selectOne(String statement) {
return original.selectOne(statement);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
return original.selectOne(statement, parameter);
}
@Override
public <E> List<E> selectList(String statement) {
return original.selectList(statement);
}
@Override
public <E> List<E> selectList(String statement, Object parameter) {
return original.selectList(statement, parameter);
}
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
return original.selectList(statement, parameter, rowBounds);
}
@Override
public <K, V> Map<K, V> selectMap(String statement, String mapKey) {
return original.selectMap(statement, mapKey);
}
@Override
public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey) {
return original.selectMap(statement, parameter, mapKey);
}
@Override
public <K, V> Map<K, V> selectMap(String statement, Object parameter, String mapKey,
RowBounds rowBounds) {
return original.selectMap(statement, parameter, mapKey, rowBounds);
}
@Override
public <T> Cursor<T> selectCursor(String statement) {
return original.selectCursor(statement);
}
@Override
public <T> Cursor<T> selectCursor(String statement, Object parameter) {
return original.selectCursor(statement, parameter);
}
@Override
public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
return original.selectCursor(statement, parameter, rowBounds);
}
@Override
public void select(String statement, Object parameter, ResultHandler handler) {
original.select(statement, parameter, handler);
}
@Override
public void select(String statement, ResultHandler handler) {
original.select(statement, handler);
}
@Override
public void select(String statement, Object parameter, RowBounds rowBounds,
ResultHandler handler) {
original.select(statement, parameter, rowBounds, handler);
}
@Override
public int insert(String statement) {
return original.insert(statement);
}
@Override
public int insert(String statement, Object parameter) {
return original.insert(statement, parameter);
}
@Override
public int update(String statement) {
return original.update(statement);
}
@Override
public int update(String statement, Object parameter) {
return original.update(statement, parameter);
}
@Override
public int delete(String statement) {
return original.delete(statement);
}
@Override
public int delete(String statement, Object parameter) {
return original.delete(statement, parameter);
}
@Override
public void commit() {
original.commit();
}
@Override
public void commit(boolean force) {
original.commit(force);
}
@Override
public void rollback() {
original.rollback();
}
@Override
public void rollback(boolean force) {
original.rollback(force);
}
@Override
public List<BatchResult> flushStatements() {
return original.flushStatements();
}
@Override
public void close() {
//original.close();
}
@Override
public void clearCache() {
original.clearCache();
}
@Override
public Configuration getConfiguration() {
return original.getConfiguration();
}
@Override
public <T> T getMapper(Class<T> type) {
return original.getMapper(type);
}
@Override
public Connection getConnection() {
return original.getConnection();
}
}
DAO 구현체 자동 생성하기
① 이전 방식
《interface》BoardDao 구현한 BoardDaoImpl 생성한다. ← 개발자가 인터페이스 규격에 맞춰 직접 구현 클래스를 정의
② 자동화 방식
DaoGenerator 에서 ① getObject(BoardDao.class) 한다. 여기서 BoardDao 구현체 ② 리턴 한다.
DaoGenerator 에서 클래스 생성에 필요한 정보를 Proxy 로 주면 인터페이스 구현체를 리턴 받는다.
BoardDao 구현체의 ① 메서드 호출 하면 여기에서 포함하고 있는 InvocationHandler 구현체에게 ② invoke (메서드 정보) 한다. ③ 리턴 받아서 ④ 전달 한다.
43. Mybatis SQL 매퍼 사용하기 II(DAO 구현 자동화 포함)
### 43. Mybatis SQL 매퍼 사용하기 II(DAO 구현 자동화 포함)
- 도메인 클래스에 별명을 부여하고 사용하는 방법
- 컬럼 이름과 객체의 프로퍼티 이름을 미리 연결해 두는 방법
- JDBC 및 DBMS 정보를 .properties 파일로 분리하는 방법
- DAO 인터페이스 구현체를 자동으로 생성하는 방법
- Mybatis 의 설정을 이용하여 DAO 구현체를 자동으로 생성하는 방법
mybatis-config.xml 에서 주소 지정하지 않고 jdbc.properties 파일에 저장하는 방법이다.
<typeAliases>
<package name=" "> 의 name에 패키지 경로 지정하면 ----Mapper.xml 파일에서 parameterType=" " 지정할때 경로 다 지정하지 않아도 된다.
<properties resource=" "> 에 jdbc.properties 파일 경로 입력하면 <property value=" "> 의 value 에 jdbc.properties 파일에 저장된 변수를 지정해 그 값을 사용한다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<properties resource="bitcamp/myapp/config/jdbc.properties"></properties>
<typeAliases>
<!-- 지정된 패키지의 모든 클래스에 대해 클래스 이름과 같은 이름으로 별명을 부여한다. -->
<package name="bitcamp.myapp.vo"/>
</typeAliases>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="bitcamp/myapp/mapper/BoardMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/MemberMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/StudentMapper.xml"/>
<mapper resource="bitcamp/myapp/mapper/TeacherMapper.xml"/>
</mappers>
</configuration>
# jdbc.properties
# name=value
jdbc.driver=org.mariadb.jdbc.Driver
jdbc.url=jdbc:mariadb://localhost:3306/studydb
jdbc.username=study
jdbc.password=1111
BoardMapper.xml 에서 ResultMap 사용해서 SQL 문에서 as 를 사용하지 않도록 하는 방법이다.
type 에 class 명, id 에 <select> 속성에서 사용할 이름을 지정한다.
pk 는 <id> 로 지정하고 나머지는 <result> 로 한다.
속성에서 column=" " 에 컬럼명 적고, property=" " 에 프로퍼티명 적는다.
<select resultMap=" "> 에서 위에서 지정한 id 를 적는다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="BoardMapper">
<resultMap type="board" id="boardMap">
<id column="board_id" property="no"/>
<result column="title" property="title"/>
<result column="content" property="content"/>
<result column="pwd" property="password"/>
<result column="created_date" property="createdDate"/>
<result column="view_cnt" property="viewCount"/>
</resultMap>
<insert id="insert" parameterType="board">
insert into app_board(title, content, pwd)
values(#{title}, #{content}, #{password})
</insert>
<select id="findAll" resultMap="boardMap">
select
board_id,
title,
created_date,
view_cnt
from
app_board
order by
board_id desc
</select>
<select id="findByNo" parameterType="int" resultMap="boardMap">
select
board_id,
title,
content,
pwd,
created_date,
view_cnt
from
app_board
where
board_id=#{no}
</select>
<update id="increaseViewCount" parameterType="int">
update app_board set
view_cnt = view_cnt + 1
where board_id=#{maumdaerohaedodoi}
</update>
<select id="findByKeyword" parameterType="string" resultMap="boardMap">
select
board_id,
title,
created_date,
view_cnt
from
app_board
where
title like(concat('%',#{keyword},'%'))
or content like(concat('%',#{keyword},'%'))
order by
board_id desc
</select>
<update id="update" parameterType="board">
update app_board set
title=#{title},
content=#{content}
where board_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_board
where board_id=#{no}
</delete>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="MemberMapper">
<resultMap type="member" id="memberMap">
<id column="member_id" property="no"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="tel" property="tel"/>
<result column="created_date" property="createdDate"/>
</resultMap>
<insert id="insert" parameterType="member"
useGeneratedKeys="true" keyProperty="no" keyColumn="member_id">
insert into app_member(name, email, pwd, tel)
values(#{name}, #{email}, sha2(#{password},256), #{tel})
</insert>
<select id="findAll" resultMap="memberMap">
select
member_id,
name,
email,
created_date
from
app_member
order by
member_id desc
</select>
<select id="findByNo" parameterType="int" resultMap="memberMap">
select
member_id,
name,
email,
tel,
created_date
from
app_member
where
member_id=#{no}
</select>
<update id="update" parameterType="member">
update app_member set
name=#{name},
email=#{email},
pwd=sha2(#{password},256),
tel=#{tel}
where member_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_member
where member_id=#{no}
</delete>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="StudentMapper">
<resultMap type="student" id="studentMap">
<id column="member_id" property="no"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="tel" property="tel"/>
<result column="created_date" property="createdDate"/>
<result column="pst_no" property="postNo"/>
<result column="bas_addr" property="basicAddress"/>
<result column="det_addr" property="detailAddress"/>
<result column="work" property="working"/>
<result column="gender" property="gender"/>
<result column="level" property="level"/>
</resultMap>
<insert id="insert" parameterType="student" >
insert into app_student(
member_id,
pst_no,
bas_addr,
det_addr,
work,
gender,
level)
values(#{no},#{postNo},#{basicAddress},#{detailAddress},#{working},#{gender},#{level})
</insert>
<select id="findAll" resultMap="studentMap">
select
m.member_id,
m.name,
m.email,
m.tel,
s.work,
s.level
from
app_student s
inner join app_member m on s.member_id = m.member_id
order by
m.name asc
</select>
<select id="findByNo" parameterType="int" resultMap="studentMap">
select
m.member_id,
m.name,
m.email,
m.tel,
m.created_date,
s.pst_no,
s.bas_addr,
s.det_addr,
s.work,
s.gender,
s.level
from
app_student s
inner join app_member m on s.member_id = m.member_id
where
m.member_id=#{no}
</select>
<select id="findByKeyword" parameterType="string" resultMap="studentMap">
select
m.member_id,
m.name,
m.email,
m.tel,
s.work,
s.level
from
app_student s
inner join app_member m on s.member_id = m.member_id
where
m.name like(concat('%', #{keyword}, '%'))
or m.email like(concat('%', #{keyword}, '%'))
or m.tel like(concat('%', #{keyword}, '%'))
or s.bas_addr like(concat('%', #{keyword}, '%'))
or s.det_addr like(concat('%', #{keyword}, '%'))
order by
m.name asc
</select>
<update id="update" parameterType="student">
update app_student set
pst_no=#{postNo},
bas_addr=#{basicAddress},
det_addr=#{detailAddress},
work=#{working},
gender=#{gender},
level=#{level}
where member_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_student
where member_id=#{no}
</delete>
</mapper>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="TeacherMapper">
<resultMap type="teacher" id="teacherMap">
<id column="member_id" property="no"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="tel" property="tel"/>
<result column="created_date" property="createdDate"/>
<result column="degree" property="degree"/>
<result column="school" property="school"/>
<result column="major" property="major"/>
<result column="wage" property="wage"/>
</resultMap>
<insert id="insert" parameterType="teacher" >
insert into app_teacher(
member_id,
degree,
school,
major,
wage)
values(#{no},#{degree},#{school},#{major},#{wage})
</insert>
<select id="findAll" resultMap="teacherMap">
select
m.member_id,
m.name,
m.tel,
t.degree,
t.major,
t.wage
from
app_teacher t
inner join app_member m on t.member_id = m.member_id
order by
m.name asc
</select>
<select id="findByNo" parameterType="int" resultMap="teacherMap">
select
m.member_id,
m.name,
m.email,
m.tel,
m.created_date,
t.degree,
t.school,
t.major,
t.wage
from
app_teacher t
inner join app_member m on t.member_id = m.member_id
where m.member_id=#{no}
</select>
<update id="update" parameterType="teacher">
update app_teacher set
degree=#{degree},
school=#{school},
major=#{major},
wage=#{wage}
where member_id=#{no}
</update>
<delete id="delete" parameterType="int">
delete from app_teacher
where member_id=#{no}
</delete>
</mapper>
DaoGenerator 로 Dao 생성할 수 있다.
BoardDaoImpl, MemberDaoImpl, StudentDaoImpl, TeacherDaoImpl 는 sql 만 일부 다르고 같은 형식이라 자동 생성하도록 설정 가능하다.
package bitcamp.util;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.apache.ibatis.session.SqlSessionFactory;
import bitcamp.myapp.dao.BoardDao;
public class DaoGenerator {
SqlSessionFactory sqlSessionFactory;
public DaoGenerator(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@SuppressWarnings("unchecked")
public <T> T getObject(Class<T> classInfo) {
String className = classInfo.getName();
return (T) Proxy.newProxyInstance(
getClass().getClassLoader(), // 현재 클래스의 로딩을 담당한 관리자: 즉 클래스 로딩 관리자
new Class[] {classInfo}, // 클래스가 구현해야 할 인터페이스 정보 목록
new MyInvocationHandler() // InvocationHandler 객체
);
}
// 자동 생성된 프록시 객체에 대해 메서드를 호출하면
// 실제 InvocationHandler의 invoke()가 호출된다.
//
class MyInvocationHandler implements InvocationHandler {
// 프록시 객체에 대해 메서드를 호출하면 이 메서드가 실행된다.
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.printf("%s() 메서드 호출했음!\n", method.getName());
if (method.getReturnType() == int.class) {
return 1;
}
return null;
}
}
public static void main(String[] args) {
DaoGenerator generator = new DaoGenerator(null);
BoardDao dao = generator.getObject(BoardDao.class);
dao.insert(null);
dao.findAll();
dao.findByNo(1);
dao.update(null);
dao.delete(1);
}
}
조언
*
과제
/