Posts 학원 #76일차: 프로젝트에 MyBatis 적용: MyBatis가 join 결과를 다루는 방법, 프로그램에서 사용하는 객체를 listener에서 준비시키기, sqlSessionFactory 공유,
Post
Cancel

학원 #76일차: 프로젝트에 MyBatis 적용: MyBatis가 join 결과를 다루는 방법, 프로그램에서 사용하는 객체를 listener에서 준비시키기, sqlSessionFactory 공유,

지난 시간에는 BoardDaoImpl에 Mybatis를 일부 적용하였다. BoardDaoImpl의 SQL을 뜯어내어 BoardMapper.xml로 옮기고, JDBC 코드를 Mybatis 클래스로 대체하였다. 그리고 mybatis-config.xmlBoardMapper 파일의 경로를 등록하였다.

오늘 수업에서는 MemberDaoImplProjectDaoImpl, TaskDaoImpl도 SLQ을 뜯어내어 XxxMaper.xml로 옮기고, JDBC 코드를 Mybatis 클래스로 대체하였다. 다만 BoardDaoImpl과 다르게 다른 테이블과의 연관성 때문에 BoardDaoImpl을 해체하는 작업보다는 난이도가 있었다.

한편, BoardDaoImpl 에서는 SqlSessionFactory를 각 메서드마다 준비하였는데, 이를 이를 AppInitListner에서 준비하도록 바꿔주었다.

실습

3단계: BoardDaoImplMybatis 적용

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 인스턴스는 공유되지 않고 스레드에 안전하지도 않다. 그러므로 가장 좋은 스코프는 요청 또는 메서드 스코프이다. SqlSessionstatic 필드나 클래스의 인스턴스 필드로 지정해서는 안 된다.

싱글턴 패턴(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에서 AppInitListenercontextInitialized()로 옮긴다.

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단계: MemberDaoImplMybatis 적용

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단계: ProjectDaoImplMybatis 적용

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 객체에다 noname을 담아야 한다. <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) 이름도 함께 출력해야 한다. 이는 어떻게 구현할 수 있을까? memberDaoprojectDao.findAll() 안에서 사용해서는 안된다. memberDaoprojectDao같은 레벨의 클래스이기 때문이다. 따라서 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 값으로 나온다.

image

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>

image

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>

위에서 알 수 있듯, ProjectDaoinsertMember를 통해 pms_member_project 테이블에 데이터를 삽입하고 있다. 즉 ProjectDaopms_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>
This post is licensed under CC BY 4.0 by the author.

학원 #75일차: Chain of Responsibility 패턴, Persistance 프레임워크 MyBatis

학원 #77일차: MyBatis 기본 문법 및 프로젝트에 적용

Loading comments from Disqus ...