지난 시간에는 BoardDaoImpl
에 Mybatis를 일부 적용하였다. BoardDaoImpl
의 SQL을 뜯어내어 BoardMapper.xml
로 옮기고, JDBC 코드를 Mybatis
클래스로 대체하였다. 그리고 mybatis-config.xml
에 BoardMapper
파일의 경로를 등록하였다.
오늘 수업에서는 MemberDaoImpl
과 ProjectDaoImpl
, TaskDaoImpl
도 SLQ을 뜯어내어 XxxMaper.xml
로 옮기고, JDBC 코드를 Mybatis
클래스로 대체하였다. 다만 BoardDaoImpl
과 다르게 다른 테이블과의 연관성 때문에 BoardDaoImpl
을 해체하는 작업보다는 난이도가 있었다.
한편, BoardDaoImpl
에서는 SqlSessionFactory
를 각 메서드마다 준비하였는데, 이를 이를 AppInitListner
에서 준비하도록 바꿔주었다.
실습
3단계: BoardDaoImpl
에 Mybatis
적용
Object 타입 자리에 원시타입 no가 오면, 컴파일러가 박싱하는 것이 아니라 컴파일러는 Integer.valueOf(no)
코드로 바꿔준다.
BoardMapper.findByNo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findByNo"
parameterType="java.lang.Integer"
resultMap="BoardMap">
select
b.no,
b.title,
b.content,
b.cdt,
b.vw_cnt,
m.no writer_no,
m.name
from
pms_board b inner join pms_member m on b.writer=m.no
where b.no = #{no}
</select>
이때 property 이름과 똑같은 이름의 컬럼이 되도록 다음과 같이 별명을 붙인다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findByNo"
parameterType="java.lang.Integer"
resultType="com.eomcs.pms.domain.Board">
select
b.no,
b.title,
b.content,
b.cdt registeredDate,
b.vw_cnt viewCount,
m.no writer_no,
m.name
from
pms_board b inner join pms_member m on b.writer=m.no
where b.no = #{no}
</select>
그러나 SQL문을 쓸 때마다 이렇게 별명을 지정하기는 귀찮다. <resultMap>
에 미리 정의해두면 사용하기 편하다. 즉 컬럼-프로퍼티의 연결 정보를 정의한다.
1
2
3
4
5
6
7
8
9
10
11
<resultMap type="com.eomcs.pms.domain.Board" id="BoardMap">
<id column="no" property="no"/>
<result column="title" property="title"/>
<result column="content" property="content"/>
<result column="cdt" property="registeredDate"/>
<result column="vw_cnt" property="viewCount"/>
<association property="writer" javaType="com.eomcs.pms.domain.Member">
<id column="writer_no" property="no"/>
<result column="name" property="name"/>
</association>
</resultMap>
이제 <select>
속성을 resultType
이 아니라 위의 resultMap="BoardMap"
으로 지정한다.
1
<select id="findByNo" parameterType="java.lang.Integer" resultMap="BoardMap">
한편, #{값}
에서 값
은 아무 이름이나 지정해도 상관 없다. #{}
문법은 JDBC에서 ?
형태로 파라미터를 전달하는 것과 같이 작동한다.
1
where b.no= #{ohora}
BoardDaoImpl.detail()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Board findByNo(int no) throws Exception {
// mybatis 객체 준비
// => mybatis 설정 파일을 읽어 입력 스트림을 준비한다.
// 입력 스트림에서 mybatis 설정 값을 읽어 SqlSessionFactory를 만든다.
SqlSessionFactory sqlSessionFactory
= new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("com/eomcs/pms/conf/mybatis/-config.xml"));
// => SqlSessionFactory에서 SqlSession 객체를 얻는다.
// SqlSession 객체는 SQL문을 실행하는 객체다.
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
Board board = sqlSession.selectOne("BoardDao.findByNo", no);
sqlSession.update("BoardDao.updateViewCount", no);
// SqlSession 객체에게 별도 파일에 분리한 SQL을 찾아 실행하라고 말한다.
return board;
}
}
여기서 어차피 update
는 한 번만 하기 때문에 오토커밋을 true로 바꾼다.
1
2
3
4
5
<update id="updateViewCount" parameterType="java.lang.Integer">
update pms_board set
vw_cnt = vw_cnt + 1
where no = #{no}
</update>
BoardDaoImpl.update()
1
2
3
4
5
6
7
8
9
@Override
public int update(Board board) throws Exception {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(
Resources.getResourceAsStream("com/eomcs/pms/conf/mybatis-config.xml"));
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
return sqlSession.update("BoardDao.update", board);
}
}
BoardMapper.update
1
2
3
4
5
6
7
<!-- update는 무조건 int니까 resultType을 적지 않아도 된다. -->
<update id="update" parameterType="com.eomcs.pms.domain.Board">
update pms_board set
title =#{title},
content=#{content},
where no = #{no}
</update>
update
의 경우 resultType
을 개발자들이 임의로 지정할 수 있으니 이를 아예 막아놨다. update
는 무조건 int
를 리턴하도록 프로그래밍 되어 있다.
4단계: App
에서 사용하는 객체를 AppInitListener
에서 모두 준비한다
현재 BoardDaoImpl
에서는 SqlSession
객체를 생성하는 객체인 SqlSessionFactory
객체를 다음과 같이 각 메서드마다 준비하였다.
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
29
30
31
public class BoardDaoImpl implements com.eomcs.pms.dao.BoardDao{
Connection con;
public BoardDaoImpl(Connection con) {
this.con = con;
}
@Override
public int insert(Board board) throws Exception {
InputStream inputStream = Resources.getResourceAsStream(
"com/eomcs/pms/conf/mybatis-config.xml");
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
return sqlSession.insert("BoardDao.insert", board);
}
}
@Override
public int delete(int no) throws Exception {
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(
Resources.getResourceAsStream("com/eomcs/pms/conf/mybatis-config.xml"));
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
return sqlSession.delete("BoardDao.delete", no);
}
}
//,,,,
현재 각 메서드마다 sqlFactory
를 빌드하고 있다. mybatis-config.xml
경로를 InputStream
Resources.getResourceAsStream()
의 파라미터로 받아 inputStream
객체를 리턴받고, 리턴받은 객체를 SqlSessionFactoryBuilder
객체의 build()
메서드의 파라미터로 넘기고 SqlSessionFactory
객체를 리턴받아 SqlSession
객체를 생성하고 있다. sqlSession
은 커밋과 관련되어 있기 때문에 다른 메서드와 공유할 수 없다. 대신 sqlSessionFactory
를 공유한다.
SqlSessionFactoryBuilder: 이 클래스는 인스턴스화되어 사용되고 던져질 수 있다.
SqlSEssionFactory
를 생성한 후 유지할 필요는 없다. 따라서 가장 좋은 스코프는 메서드 스코프(지역변수)이다. 여러 개의SqlSessionFactory
인스턴스를 빌드하기 위해SqlSessionFactoryBuilder
를 재사용할 수는 있지만 유지하지 않는 것이 좋다.
SqlSessionFactory: 한 번 만든 뒤
SqlSessionFactory
는 애플리케이션을 실행하는 동안 존재하여야 한다. 그래서 삭제하거나 재생성할 필요가 없다. 애플리케이션이 실행되는 동안 여러 차례SqlSessionFactory
를 다시 빌드하지 않는 것이 가장 좋은 형태이다.SqlSessionFactory
의 가장 좋은 스코프는 애플리케이션 스코프이다. 애플리케이션 스코프로 유지하기 위해서는 싱글턴 패턴이나static
싱글턴 패턴을 사용하는 방법, 또는 구글 쥬스나 스프링과 같은 의존성 삽입 컨테이너는SqlSessionFactory
의 생명주기를 싱글턴으로 관리한다.
공유될 수 없는 SqlSession: 각각의 스레드는 자체적으로
SqlSession
인스턴스를 가져야 한다.SqlSession
인스턴스는 공유되지 않고 스레드에 안전하지도 않다. 그러므로 가장 좋은 스코프는 요청 또는 메서드 스코프이다.SqlSession
을static
필드나 클래스의 인스턴스 필드로 지정해서는 안 된다.
싱글턴 패턴(Singleton pattern): 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고, 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 주로 공통된 객체를 여러개 생성해서 사용하는
DBCP
(Database Connection Pool)와 같은 상황에서 많이 사용된다.
출처: https://mybatis.org/mybatis-3/ko/getting-started.html
공식 문서에 따르면 SqlSessionFactory
는 애플리케이션 스코프로 유지하는 것이 좋다고 한다. 따라서 XxxDao
클래스의 각 메서드에서 SqlSessionFactory
를 준비하는 대신에 XxxDao
의 생성자의 파라미터로 주입받을 수 있도록 코드를 바꾸도록 하자. 따라서 시스템에서 사용할 객체를 준비하는 AppInitListener
에서 이 일을 하도록 하자.
com.eomcs.pms.listener.AppInitListener
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AppInitListener implements ApplicationContextListener {
@Override
public void contextInitialized(Map<String,Object> context) {
System.out.println("프로젝트 관리 시스템(PMS)에 오신 걸 환영합니다!");
// 시스템에서 사용할 객체를 준비한다.
try {
Connection con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/studydb?user=study&password=1111");
context.put("con", con);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(
Resources.getResourceAsStream("com/eomcs/pms/conf/mybatis-config.xml"));
context.put("sqlSessionFactory", sqlSessionFactory);
} catch (Exception e) {
System.out.println("DB 커넥션을 준비하는 중에 오류 발생");
e.printStackTrace();
}
}
XxxDao
구현체 생성 코드도 App
에서 AppInitListener
의 contextInitialized()
로 옮긴다.
1
BoardDao boardDao = new BoardDaoImpl(sqlSessionFactory);
App
에 있던 Command
구현체 생성 코드도 이 메서드로 옮긴다.
1
2
3
4
5
6
7
8
9
Map<String,Command> commandMap = new HashMap<>();
commandMap.put("/board/add", new BoardAddCommand(boardDao, memberDao));
commandMap.put("/board/list", new BoardListCommand(boardDao));
commandMap.put("/board/detail", new BoardDetailCommand(boardDao));
commandMap.put("/board/update", new BoardUpdateCommand(boardDao));
commandMap.put("/board/delete", new BoardDeleteCommand(boardDao));
//..
context.put("commandMap", commandMap);
com.eomcs.pms.App
에서 DAO
구현체 생성 코드와 Command
구현체 생성 코드를 제거한다. commandMap
객체 생성 코드도 제거한다.
이제 App
에서는 Command
를 실행하는 코드는 찾아볼 수 없다. 이렇게 되면 실제 명령을 하면 어떤 작업이 수행되는지 바로바로 추적하기가 힘들다. 감춰져 있기 때문이다. 우리가 오픈소스를 봤을 때 코드를 쉽게 이해하기 힘든 이유가 다른 프로젝트도 이런 식으로 커맨드가 감춰져있기 때문이다. 이처럼 이해가 쉽지 않은 단점이 있지만, 향후 새로운 커맨드를 추가할 때 더이상 App
클래스를 직접 손댈 필요가 없어 유지보수가 편하다는 장점이 있다. 새로운 커맨드를 추가하려면 그냥 AppInitListener
에 한 줄만 추가하면 된다.
VO
(Value Object; 값 객체): 값을 위해 쓴다. 자바는 값 타입을 표현하기 위해 불변 클래스를 만들어 사용한다. 불변 클래스라 하면,readOnly
특징을 가진다. 이러한 클래스는 중간에 값을 바꿀 수 없고, 새로 만들어야 한다.
실무에서는
VO
와 도메인 객체를 혼용해서 사용하는 듯하다.
5단계: MemberDaoImpl
에 Mybatis
적용
com.eomcs.pms.dao.mariadb.MemberDaoImpl
JDBC 코드를 뜯어 내고 그 자리에 Mybatis
클래스로 대체한다.
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class MemberDaoImpl implements com.eomcs.pms.dao.MemberDao {
SqlSessionFactory sqlSessionFactory;
public MemberDaoImpl(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
@Override
public int insert(Member member) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
return sqlSession.insert("MemberDao.insert", member);
}
}
@Override
public int delete(int no) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
return sqlSession.delete("MemberDao.delete", no);
}
}
@Override
public Member findByNo(int no) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
return sqlSession.selectOne("MemberDao.findByNo", no);
}
}
@Override
public Member findByName(String name) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
List<Member> members = sqlSession.selectList("MemberDao.findByName", name);
if (members.size() > 0) {
return members.get(0);
} else {
return null;
}
}
}
@Override
public List<Member> findAll() throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("MemberDao.findAll");
}
}
// select는 값은 조회하는 것이기 때문에 autocommit을 설정할 필요가 없다.
@Override
public int update(Member member) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
return sqlSession.update("MemberDao.update", member);
}
}
@Override
public List<Member> findByProjectNo(int projectNo) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList("MemberDao.findByProjectNo", projectNo);
}// select는 값은 조회하는 것이기 때문에 autocommit을 설정할 필요가 없다.
}
@Override
public Member findByEmailPassword(String email, String password) throws Exception {
HashMap<String,Object> map = new HashMap<>();
map.put("email", email);
map.put("password", password);
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectOne("MemberDao.findByEmailPassword", map);
}// select는 값은 조회하는 것이기 때문에 autocommit을 설정할 필요가 없다.
}
}
com/eomcs/pms/mapper/MemberMapper.xml
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="MemberDao">
<resultMap type="com.eomcs.pms.domain.Member" id="MemberMap">
<id column="no" property="no"/>
<result column="name" property="name"/>
<result column="email" property="email"/>
<result column="photo" property="photo"/>
<result column="tel" property="tel"/>
<result column="cdt" property="registeredDate"/>
</resultMap>
<select id="findAll" resultMap="MemberMap">
select
no,
name,
email,
tel,
cdt
from
pms_member
order by
no desc
</select>
<insert id="insert" parameterType="com.eomcs.pms.domain.Member">
insert into pms_member(name,email,password,photo,tel)
values(#{name},#{email},password(#{password}),#{photo},#{tel})
</insert>
<select id="findByNo"
parameterType="java.lang.Integer"
resultMap="MemberMap">
select
no,
name,
email,
photo,
tel,
cdt
from
pms_member
where
no = #{no}
</select>
<select id="findByName"
parameterType="java.lang.String"
resultMap="MemberMap">
select
no,
name,
email,
photo,
tel,
cdt
from
pms_member
where
name = #{name}
</select>
<select id="findByProjectNo"
parameterType="java.lang.Integer"
resultMap="MemberMap">
select
m.no,
m.name
from
pms_member_project mp
inner join pms_member m on mp.member_no=m.no
where
mp.project_no=#{no}
order by
m.name asc
</select>
<select id="findByEmailPassword"
parameterType="java.util.Map"
resultMap="MemberMap">
select
no,
name,
email,
photo,
tel,
cdt
from
pms_member
where
email = #{email}
and password = password(#{password})
</select>
<update id="update" parameterType="com.eomcs.pms.domain.Member">
update pms_member set
name = #{name},
email = #{email},
password = password(#{password}),
photo = #{photo},
tel = #{tel}
where no = #{no}
</update>
<delete id="delete" parameterType="java.lang.Integer">
delete from pms_member
where no=#{no}
</delete>
</mapper>
6단계: ProjectDaoImpl
에 Mybatis
적용
pms.domain.Project
를 확인해보자.
1
2
3
4
5
6
7
8
9
10
11
12
public class Project {
//..
private Member owner;
private List<Member> members;
//..
public void setOwner(Member owner) {
this.owner = owner;
}
public void setMembers(List<Member> members) {
this.members = members;
}
}
Project
클래스는 Member
타입의 프로퍼티가 있다. 이때 Member
객체에다 no
와 name
을 담아야 한다. <resultMap>
태그 안에 <association>
태그를 사용하면 Project
객체를 만들 때 컬럼 값을 Member
도메인 객체에 어떻게 저장할 지 지정할 수 있다.
컬럼-프로퍼티의 연결 정보 지정하기
컬럼의 값을 어떤 자바 객체에 담을 것인지 <resultMap>
태그를 통해서 따로 정의할 수 있다. type
속성은 자바 클래스명을, id
속성의 값으로는 컬럼-프로퍼티의 연결 정보를 가리킬 이름을 적는다.
1
2
3
4
5
6
7
8
9
10
11
12
<resultMap type="com.eomcs.pms.domain.Project"
id="ProjectMap">
<result column="no" property="no" />
<result column="title" property="title" />
<result column="content" property="content" />
<result column="sdt" property="startDate" />
<result column="edt" property="endDate" />
<association property="owner" javaType="com.eomcs.pms.domain.Member">
<result column="owner_no" property="owner" />
<result column="owner_name" property="owner" />
</association>
</resultMap>
자바 코드로는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
Project p = new Project();
p.setNo(rs.getInt("no"));
p.setTitle(rs.getString("title"));
p.setRegisteredDate(rs.getDate("cdt"));
p.setViewCount(rs.getInt("vw_cnt"));
Member m = new Member();
m.setNo(rs.getInt("no"));
m.setName(rs.getString("name"));
p.setOwner(m);
즉 <association>
은 Mybatis에게 Member
객체를 생성해 setName()
, setNo()
(프로퍼티)라는 메서드를 호출해서 컬럼값을 프로퍼티에 담는다. 이렇게 컬럼값이 담겨진 Member
객체는 setOwner()
를 호출해 Project
객체의 owner
프로퍼티에 담는다.
기존 projectListCommand
는 프로젝트 목록을 출력할 때 각 프로젝트에 해당하는 팀원(Member
) 이름도 함께 출력해야 한다. 이는 어떻게 구현할 수 있을까? memberDao
를 projectDao.findAll()
안에서 사용해서는 안된다. memberDao
와 projectDao
는 같은 레벨의 클래스이기 때문이다. 따라서 ProjectListCommmand
클래스 안에서 MemberDao
객체의 findByProjectNo()
메서드를 호출하고, (MemberDao
를 사용하고) ProjectDao
객체의 findAll()
메서드를 호출하는 것이 맞다.
이를 위해서 ProjectDao
객체뿐만 아니라 MemberDao
객체도 생성자로 주입받도록 만든다.
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
29
30
31
32
33
34
35
36
37
38
39
40
41
public class ProjectListCommand implements Command {
ProjectDao projectDao;
MemberDao memberDao;
public ProjectListCommand(ProjectDao projectDao, MemberDao memberDao) {
this.projectDao = projectDao;
this.memberDao = memberDao;
}
@Override
public void execute(Map<String,Object> context) {
System.out.println("[프로젝트 목록]");
try {
List<Project> list = projectDao.findAll();
System.out.println("번호, 프로젝트명, 시작일 ~ 종료일, 관리자, 팀원");
for (Project project : list) {
StringBuilder members = new StringBuilder();
for (Member member : memberDao.findByProjectNo()) {
if (members.length() > 0) {
members.append(",");
}
members.append(member.getName());
}
System.out.printf("%d, %s, %s ~ %s, %s, [%s]\n",
project.getNo(),
project.getTitle(),
project.getStartDate(),
project.getEndDate(),
project.getOwner().getName(),
members.toString());
}
} catch (Exception e) {
System.out.println("프로젝트 목록 조회 중 오류 발생!");
e.printStackTrace();
}
}
}
이 방법은projectDao.findAll
을 할 때 한 번 select
를 하는 것이 아니라, 각각의 프로젝트에 대해 select
를 해야 한다. 이렇게 하는 것이 아니라 한 번 select
를 하고 Project
객체를 리턴할 때 members
프로퍼티까지 채워서 리턴할 수는 없을까? owner
프로퍼티처럼 말이다. 이를 위해서는 findAll
메서드에서 select
를 할 때 join을 두 번 해야 한다.
현재 owner
프로퍼티만을 채웠다. Member
객체의 setNo()
와 setName()
을 하기 위해서 다음과 같이 pms_member
테이블과 inner join을 했다.
select
p.no,
p.title,
p.sdt,
p.edt,
m.no owner_no,
m.name owner_name
from
pms_project p
inner join pms_member m on p.owner=m.no
order by p.no desc
실행 결과는 다음과 같다. 프로젝트를 등록할 때 owner_name
는 반드시 있으므로 데이터가 누락될 염려는 없다. 문제는 members
프로퍼티를 채우기 위해 pms_member_project
와 조인할 때 나타난다. members
는 값이 없을 수 있기 때문이다.
+----+---------+------------+------------+----------+------------+
| no | title | sdt | edt | owner_no | owner_name |
+----+---------+------------+------------+----------+------------+
| 15 | project | 2020-01-02 | 2020-02-02 | 1 | hayeon |
| 14 | test200 | 2020-01-01 | 2020-01-02 | 6 | x1 |
members 프로퍼티를 채우기 위해서는 pms_member_project
테이블과도 조인을 해야 한다. 따라서 다음과 같이 inner join을 두 번 해주었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
select
p.no,
p.title,
p.sdt,
p.edt,
m.no owner_no,
m.name owner_name,
mp.member_no
from
pms_project p
inner join pms_member m on p.owner=m.no
inner join pms_member_project mp on p.no=mp.project_no
order by p.no desc
위와 같이 inner join을 한다면 조건과 일치하는 데이터가 없다면 누락된다. 따라서 팀원이 없는 프로젝트라면 해당 프로젝트는 조인된 테이블에 나오지도 않을 것이다. 이때 사용하는 것이 outer join이다. 프로젝트를 기준점으로 left outer join을 한다. outer join은 일치하는 데이터가 없다고 하더라도 기준이 되는 데이터는 무조건 나오게 된다. 단 일치하지 않는 컬럼값은 null 값으로 나온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
select
p.no,
p.title,
p.sdt,
p.edt,
m.no owner_no,
m.name owner_name,
mp.member_no
from
pms_project p
inner join pms_member m on p.owner=m.no
left outer join pms_member_project mp on p.no=mp.project_no
order by p.no desc
여기에 팀원들의 이름까지 출력하고자 한다면 다음과 같이 sql문을 짠다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="findAll" resultMap="ProjectMap">
select
p.no,
p.title,
m.no owner_no,
m.name owner_name,
mp.member_no,
m2.name member_name
from
pms_project p
inner join pms_member m on p.owner=m.no
left outer join pms_member_project mp on p.no=mp.project_no
left outer join pms_member m2 on mp.member_no=m2.no
order by p.no desc
</select>
mybatis
의<id>
태그는 PK 처럼 작동한다. 번호가 같으면 같은 객체로 간주한다. 만약 title 컬럼을 id로 지정하면 title이 같으면 같은 데이터로 취급한다. 습관적으로 여러 테이블을 조인할 때도 primary key 컬럼을 id 태그로 지정하라. 객체를 만들 때는 반드시 누가 pk인지 id로 지정하라.
ProjectDao.insert()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public int insert(Project project) throws Exception {
// 트랜젝션을 위해 오토커밋을 비활성화한다.
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// 프로젝트 정보 입력
int count = sqlSession.insert("ProjectDao.insert", project);
// 프로젝트의 멤버 정보 입력
for (Member member : project.getMembers()) {
HashMap<String,Object> map = new HashMap<>();
map.put("memberNo", member.getNo());
// 객체에 프로젝트 no 프로퍼티가 등록되어있어야 한다.
map.put("projectNo", project.getNo());
sqlSession.insert("ProjectDao.insertMember", map);
}
// 묶인 작업이 다 끝났을 때 커밋을 한다.
sqlSession.commit();
return count;
}
}
ProjectMapper.insert
1
2
3
4
5
6
7
8
<insert id="insert" parameterType="com.eomcs.pms.domain.Project">
insert into pms_project(title,content,sdt,edt,owner)
values(#{title},#{content},#{startDate},#{endDate},#{owner.no})
</insert>
<insert id="insertMember" parameterType="java.util.Map">
insert into pms_member_project(member_no, project_no)
values(#{memberNo},#{projectNo})
</insert>
위에서 알 수 있듯, ProjectDao
는 insertMember
를 통해 pms_member_project
테이블에 데이터를 삽입하고 있다. 즉 ProjectDao
는 pms_member_project
의 owner이다. 따라서 다른 DAO
는 owner가 되면 안된다. 한 테이블에 대해서 한 DAO만 insert, update, delete를 해야지 여러 DAO가 owner가 되면 유지보수가 어렵기 때문이다.
한편, ProjectDao의 insert()
메서드의 파라미터로 넘어온 Project
객체에는 사용자가 입력한 프로젝트 title, startDate, endDate, owner가 들어 있는데, 프로젝트의 no는 들어 있지 않다(혹은 int의 기본값이 0이 들어 있다). 이 no값은 DBMS에서 자동으로 증가하는 값이기 때문이다.
따라서 이 상태에서 프로젝트를 추가하려고 시도하면 다음과 같은 에러가 나온다.
1
Cannot add or update a child row: a foreign key constraint fails (`studydb`.`pms_member_project`, CONSTRAINT `pms_member_project_fk2` FOREIGN KEY (`project_no`) REFERENCES `pms_project` (`no`))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
// no 파라미터가 비어 있는 project 객체
public int insert(Project project) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// insert를 하면 project 객체의 no 값이 채워져야 한다.
int count = sqlSession.insert("ProjectDao.insert", project);
for (Member member : project.getMembers()) {
HashMap<String,Object> map = new HashMap<>();
map.put("memberNo", member.getNo());
// 객체에 프로젝트 no 프로퍼티가 등록되어있어야 한다.
map.put("projectNo", project.getNo());
sqlSession.insert("ProjectDao.insertMember", map);
}
sqlSession.commit();
return count;
}
}
이를 위해서는 insert
태그에 특별한 속성을 지정해야 한다.
1
2
3
4
5
<insert id="insert" parameterType="com.eomcs.pms.domain.Project"
useGeneratedKeys="true" keyColumn="no" keyProperty="no">
insert into pms_project(title,content,sdt,edt,owner)
values(#{title},#{content},#{startDate},#{endDate},#{owner.no})
</insert>
위와 같이 userGeneratedKeys
, keyColumn
, keyProperty
속성을 지정한다. 파라미터로 객체(의 주소)를 넘겨주면 mybatis는 객체에 기록되어 있는 정보를 바탕으로 데이터베이스에 저장하는 것으로 끝나느 것이 아니라, 객체에 setNo()
로 기록한다. 따라서 우리는 자동 증가된 값을 getNo()
받을 수 있다. 내가 객체의 주소를 넘겨준다는 것은 나도 이 주소를 사용할 수 있다는 뜻이다. 즉 객체는 원시타입처럼 직접 넘겨주는 것이 아니라 주소를 넘겨주기 때문에 가능한 일이다.객체는 Heap 메모리에 있고, 주소를 가지고 공유할 수 있기 때문에 가능하다. 따라서 프로그램이 getNo()
를 가지고 사용하면 된다. 이제 project.getNo()
를 호출하여 pms_member_project 테이블에 프로젝트 번호를 저장할 수 있게 되었다.
수동 커밋일 때는 sqlSession.commit()
을 호출해야 작업한 내용이 DBMS에 반영된다.
ProjectDao.delete()
1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public int delete(int no) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
// 프로젝트에 소속된 모든 멤버를 삭제한다.
sqlSession.delete("ProjectDao.deleteMembers", no);
// => 프로젝트를 삭제한다.
int count = sqlSession.delete("ProjectDao.delete", no);
sqlSession.commit();
return count;
}
}
ProjectMapper.delete
1
2
3
4
5
6
7
8
9
<delete id="deleteMembers" parameterType="java.lang.Integer">
delete from pms_member_project
where project_no=#{no}
</delete>
<delete id="delete" parameterType="java.lang.Integer">
delete from pms_project
where no=#{no}
</delete>
ProjecDao.update()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Override
public int update(Project project) throws Exception {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
int count = sqlSession.update("ProjectDao.update", project);
if (count == 0) {
return 0;
}
// 프로젝트 팀원 변경한다.
// => 기존에 설정된 모든 팀원을 삭제한다.
sqlSession.delete("ProjectDao.deleteMembers", project.getNo());
// => 새로 팀원을 입력한다.
for (Member member : project.getMembers()) {
HashMap<String,Object> map = new HashMap<>();
map.put("memberNo", member.getNo());
map.put("projectNo", project.getNo());
sqlSession.insert("ProjectDao.insertMember", map);
}
sqlSession.commit();
return 1;
}
}
ProjectMapper.update
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<update id="update" parameterType="com.eomcs.pms.domain.Project">
update pms_project set
title = #{title},
content = #{content},
sdt = #{startDate},
edt = #{endDate},
owner = #{owner.no}
where no = #{no}
</update>
<delete id="deleteMembers" parameterType="java.lang.Integer">
delete from pms_member_project
where project_no=#{no}
</delete>
<insert id="insertMember" parameterType="java.util.Map">
insert into pms_member_project(member_no, project_no)
values(#{memberNo},#{projectNo})
</insert>