본문 바로가기

카테고리 없음

2020.11.10 일자 수업 : MyBatis

 실습 - MyBatis 

 

어제 수업에서 진행한 실습에 이어서 BoardDao의 findByNo, update, delete 메서드에서 사용하는 SQL문을 MyBatis로 처리한다.

 

BoardDao - findByNo

일단 해당 메서드에서 사용하는 SQL문을 BoardMapper.xml 파일안의 새로운 select 태그 안으로 가져온다. 구체적인 내용은 다음과 같다.

  • id 속성은 findByNo로 지정하고, parameterType은 번호를 가져올 것이니 래퍼 클래스인 Integer 클래스로 지정한다. resultMap은 이전에 설정해놓은 BoardMap으로 지정한다.
  • SQL문을 집어넣고, ? 자리에 #{no}를 작성한다. 래퍼클래스나 primitive 타입을 파라미터로 받아 SQL 문에 삽입하는 경우에는 #{} 안에 어떤 이름을 집어넣어도 상관없으나, 의미적인 가독성을 위하여 게시물의 번호임을 알 수 있는 이름으로 정한다.
 <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>

또 게시물에 대해서 BoardDao가 findByNo를 호출할 때마다 해당 게시글의 조회수를 하나씩 올려야 하므로 이를 위한 SQL 문을 Mapper 파일에 추가한다. id는 updateViewCount고, parameterType은 게시물의 번호이므로 Integer 클래스이다.

 <update id="updateViewCount" parameterType="java.lang.Integer">
  update pms_board set
  vw_cnt = vw_cnt + 1
  where no = #{no}
 </update>

BoardMapper.xml에 SQL을 추가했으니 이제 BoardDao에서 SqlSessionFactory을 생성하고, 이에 대하여 openSession을 호출하여 SqlSession을 리턴받는다. 파라미터는 true값을 주어 auto-commit을 true로 설정한다. 그리고 이 sqlSession에 대하여 파라미터로 Mapper 파일에서 추가한 Sql의 경로와 게시물의 번호를 넘겨주고 select를 호출한다. 그런 후에는 이 게시물의 조회수를 하나 올리는 updateViewCount SQL문의 경로와 게시물의 번호를 주고 update를 호출한다.

  @Override
  public Board findByNo(int no) throws Exception {
    SqlSessionFactory sqlSessionFactory =
        new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream(
            "com/eomcs/pms/conf/mybatis-config.xml"));
    try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
      Board board = sqlSession.selectOne("BoardDao.findByNo", no);
      sqlSession.update("BoardDao.updateViewCount", no);
          return board;
    }
  }

BoardDao - update

BoardDao의 update 메서드에서 사용하는 update SQL문을 Mapper 파일의 update 태그안으로 가져온다. id는 update이고 parameterType은 변경할 정보를 담은 Board 객체를 파라미터로 줄 것이므로 Board 클래스로 지정한다.

 <update id="update" parameterType="com.eomcs.pms.domain.Board">
   update pms_board set 
     title = #{title},
     content = #{content} 
   where no = #{no}
 </update>

SQL문을 추가했다면 전과 마찬가지로, SqlSessionFactory를 생성 후, SqlSession를 리턴받아 이에 대해 Mapper 파일에서 전에 추가한 SQL문을 실행하는 update문을 작성한다. 두번째 파라미터로 새로 변경된 Board 객체를 넘기는 것도 잊지 않는다.

  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);
    }
  }

BoardDao - delete

delete 태그 안에 BoardDao의 delete메서드에서 사용 중인 SQL문을 가져와서 작성한다. id는 delete이고, parameterType은 게시물의 번호이므로 Integer 클래스로 지정한다.

 <delete id="delete" parameterType="java.lang.Integer">
    delete from pms_board 
    where no=#{no}
 </delete>

이제 BoardDao의 delete 메서드에서 SqlSessionFactory와 SqlSession를 차례로 생성후 SqlSession에 대하여 해당 SQL문을 실행하는 delete 메서드를 호출한다. 두번째 파라미터로는 delete 메서드에서 파라미터로 받은 게시물의 번호를 넘겨준다.

  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);
    }
  }

*update Insert delete 와같이 executeUpdate 메서드를 내부적으로 호출하는 메서드들은 이 executeUpdate가 리턴하는 int값을 그대로 리턴한다. 따라서 parameterType 속성을 사용자가 원하는 대로 지정할수가 없다.


 실습 - SqlSessionFactory 공유 

 

SqlSessionFactory는 이왕이면 MyBatis를 사용하는 동안 최대한 적게 생성하는 것이 좋다. 최고는 단 한개의 객체만 생성하는 것으로 싱글톤 디자인 패턴이 적합하다. 

출처 : mybatis.org/mybatis-3/ko/getting-started.html

 

MyBatis – 마이바티스 3 | 시작하기

Copyright © 2009–2020MyBatis.org. .

mybatis.org

우리도 외부에서 하나의 SqlSessionFactory를 하나만 생성하고 그것을 각 DAO 구현체에서 공유하는 구조로 수정할 것이다. 이를 위해서는 일단 각 메서드에서 생성하고 있는 SqlSessionFactory 객체를 인스턴스 필드로 지정하고, 생성자에서 SqlSessionFactory 객체를 받아 이 필드에 저장할 것이다.

public class TaskDaoImpl implements TaskDao {

  Connection con;
  SqlSessionFactory sqlSessionFactory;

  public TaskDaoImpl(Connection con, SqlSessionFactory sqlSessionFactory) {
    this.con = con;
    this.sqlSessionFactory = sqlSessionFactory;
  }

이렇게 해주면 각 메서드에서 SqlSessionFactory 를 생성하는 코드를 삭제하고 이 필드에 저장된 객체를 통해 바로 SqlSession을 리턴받을 수가 있다.

  @Override
  public int insert(Board board) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
      return sqlSession.insert("BoardDao.insert", board);
    }
  }

이제 Dao를 사용하는 측에서 SqlSessionFactory 객체를 생성하여 생성자의 파라미터로 넘겨주기만 하면 된다. 이처럼 특정 클래스에서 작업을 수행하는 데 필요한 자원을 외부로부터 주입받는 것을 DI(Dependancy injection)라고 한다.

 

그런데 이렇게 넣어준 김에 App에서 직접생성했던 SqlSessionFactory, Dao 구현체, Command 구현체를 모두 생성하고, 이 모든 객체들을 사용하는 Command 객체들은 context에 넣어주면 App이 실행되는 시점에 사용자의 명령에 따라 작업을 수행할 준비를 마칠 수 있게 된다.

public class DefaultCommandFilter implements CommandFilter {

  @SuppressWarnings("unchecked")
  @Override
  public void doFilter(Request request, FilterChain nextFilter) throws Exception {
    Map<String, Object> context = request.getContext();

    Map<String, Command> commandMap = (Map<String, Command>) context.get("commandMap");

    Command command = commandMap.get(request.getCommandPath());
    if (command != null) {
      try {
        command.execute(context);
      } catch (Exception e) {
        // 오류가 발생하면 그 정보를 갖고 있는 객체의 클래스 이름을 출력한다.
        System.out.println("--------------------------------------------------------------");
        System.out.printf("명령어 실행 중 오류 발생: %s\n", e);
        System.out.println("--------------------------------------------------------------");
      }
    } else {
      System.out.println("실행할 수 없는 명령입니다.");
    }

  }
}

 

심지어 DefaultCommandFilter에서 각 Command 객체들을 꺼내어 실행시키기때문에 App 클래스에서 모든 Dao와 Command 구현체들이 완전히 숨겨지게 된다.

 

public class App {

  Map<String,Object> context = new Hashtable<>();

  List<ApplicationContextListener> listeners = new ArrayList<>();

  public void addApplicationContextListener(ApplicationContextListener listener) {
    listeners.add(listener);
  }

  public void removeApplicationContextListener(ApplicationContextListener listener) {
    listeners.remove(listener);
  }

  private void notifyApplicationContextListenerOnServiceStarted() {
    for (ApplicationContextListener listener : listeners) {
      listener.contextInitialized(context);
    }
  }

  private void notifyApplicationContextListenerOnServiceStopped() {
    for (ApplicationContextListener listener : listeners) {
      listener.contextDestroyed(context);
    }
  }


  public static void main(String[] args) throws Exception {
    App app = new App();

    app.addApplicationContextListener(new AppInitListener());

    app.service();
  }

  public void service() throws Exception {

    notifyApplicationContextListenerOnServiceStarted();
    
    CommandFilterManager filterManager = new CommandFilterManager();
    
    filterManager.add(new LogCommandFilter(new File("command.log")));
    filterManager.add(new AuthCommandFilter());
    filterManager.add(new DefaultCommandFilter());
    
    filterManager.init(context);

    Deque<String> commandStack = new ArrayDeque<>();
    Queue<String> commandQueue = new LinkedList<>();
    

    loop:
      while (true) {
        String inputStr = Prompt.inputString("명령> ");

        if (inputStr.length() == 0) {
          continue;
        }

        commandStack.push(inputStr);
        commandQueue.offer(inputStr);


        switch (inputStr) {
          case "history": printCommandHistory(commandStack.iterator()); break;
          case "history2": printCommandHistory(commandQueue.iterator()); break;
          case "quit":
          case "exit":
            System.out.println("안녕!");
            break loop;
          default:
            Request request = new Request(inputStr, context);
            
            filterManager.doFilter(request);
            
        }
        System.out.println();
      }

    Prompt.close();
    filterManager.destroy();

    notifyApplicationContextListenerOnServiceStopped();
  }

  void printCommandHistory(Iterator<String> iterator) {
    try {
      int count = 0;
      while (iterator.hasNext()) {
        System.out.println(iterator.next());
        count++;

        if ((count % 5) == 0 && Prompt.inputString(":").equalsIgnoreCase("q")) {
          break;
        }
      }
    } catch (Exception e) {
      System.out.println("history 명령 처리 중 오류 발생!");
    }
  }
}

 실습 - MyBatis 2 

 

MemberDao - findByName

BoardDao를 MyBatis를 사용하게 처리하는 것을 완료했으니 이어서 MemeberDao를 처리한다. 코드를 수정하는 과정에서 전에 새롭게 배우는 것만을 이곳에 정리할 것이다.

 

SqlSession에 대하여 select SQL문을 실행하는 메서드를 호출할 때, 두 개 이상의 결과가 나올 수 있는 상황에서 selectOne을 호출하면 에러가 뜬다. findByName도 마찬가지로 데이터베이스에서 같은 이름을 가진 회원을 List에 묶어 리턴할 가능성이 있으므로 selectList를 호출한 다음, 이 List에 대하여 첫번째 객체를 꺼내 리턴한다.

public Member findByName(String name) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
      return (Member) sqlSession.selectList("MemberDao.findByName", name).get(0);
    }
  }

MemberDao - findByEmailPassword

findByEmailPassword 메서드에서는 주어진 email과 password 두개의 정보와 일치하는 Member를 꺼내야하기 때문에 이 두 개의 데이터들을 Map 객체에 담아 select 메서드의 두 번째 파라미터로 넘겨줘야 한다.

 @Override
  public Member findByEmailPassowrd(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(true)) {
      return sqlSession.selectOne("MemberDao.findByEmailPassowrd", map);
    }
  }

그리고 Mapper에서 이 두 정보를 갖고 데이터를 찾아내는 SQL문을 정의하려면 parameterType 속성으로는 HashMap으로 지정하고, 이 Map에 데이터를 저장할 때 함께 저장한 키를 #{} 안에 지정해줘야 한다.

 <select id="findByEmailPassowrd" parameterType="java.util.HashMap"
  resultMap="MemberMap">
   select 
     no, 
     name, 
     email, 
     photo, 
     tel, 
     cdt
   from pms_member
   where email= #{email}
     and password=password(#{password})
 </select>

association property는 프로젝트의 필드명, javaType 필드의 타입, 따라서 이 객체를 생성하여 지정된 값을 이 객체의 프로퍼티에 담고, 이 객체를 owner라는 프로퍼터에 담는 것이다. 


ProjectDao-findAll

ProjectDao부터는 이 객체가 관할하는 테이블인 pms_project에서 외부키가 pms_member에 연결되어있고, pms_member_project에서는 외부키가 pms_member과 pms_project에 연결되어있으므로 다소 복잡해진다. 

 

일단 ProjectDao의 findAll부터 수정해보자. pms_project에서 owner 컬럼은 pms_member의 no 컬럼과 연결된 외부키이다. 그냥 owner 컬럼 값을 꺼낼 수도 있지만, Project 도메인 클래스에서 owner에 해당하는 필드의 타입은 Member 타입이고, findAll 메서드를 사용하는 ProjectListCommand 측에서는 이 owner의 번호뿐만 아니라 이름까지 요구한다. 따라서 pms_member와의 inner join을 통해 owner의 이름 값까지 꺼낸다.

 <select id="findAll" resultMap="ProjectMap">
  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
 </select>

이 SQL문을 통해 얻은 결과들을 ResultMap 태그를 통해 자바 객체에 담는 코드는 다음과 같다. 

  • 일단 resultMap의 type 속성값은 정보를 담을 도메인 클래스인 Project로 지정한다. id 속성값은 ProjectMap이다.
  • result 태그를 통해 각각의 정보를 프로퍼티와 연결한다. 단, pk가 되는 no는 id 태그로 지정한다.
  • owner 정보를 담아야할 필드의 타입이 Member이므로, associtation  태그를 사용하여 별도의 Member 객체를 만들어 그것을 필드에 담아야 한다. property 속성 값으로는 Project에서 해당 정보가 들어갈 프로퍼티명인 owner로 지정하고, javaType 속성값으로는 필드를 담을 클래스 타입인 Member 클래스명을 지정한다. 이렇게 하면 Member 객체를 생성한 뒤, 이 객체를 owner에 담는다.
  • association 태그 안에는 생성된 Memeber 객체에 대하여 어떤 결과 값을 어떤 프로퍼티에 저장할 것인지를 작성한다. 이것은 우리가 아는 대로 컬럼과 프로퍼티를 매핑 시켜주면 된다. 단, 여기서도 pk 가 되는 no는 id 태그로 연결한다.
<resultMap type="com.eomcs.pms.domain.Project"
  id="ProjectMap">
  <id 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">
   <id column="owner_no" property="no" />
   <result column="owner_name" property="name" />
  </association>
</resultMap>

ResultMap에서 Id의 역할

  • 정보 캐싱을 통한 성능 강화 :  id를 지정하면 이것을 기반으로 mybatis가 힙에 위치하는 캐시에 정보를 저장하여 나중에 같은 정보를 select할 경우, 이 정보를 사용하므로 성능이 좋아진다.
  • id를 기준으로 객체 묶기 : id를 기준으로 객체를 묶기 때문에 id를 설정하지 않으면, 같은 테이블 내에서 하나라도 컬럼 값이 다르면 각각의 객체를 생성하나, id를 설정하면 같은 테이블 내에서 id를 제외한 다른 컬럼 값이 다르더라도 이를 무시하고 같은 객체에 정보를 담는다. 이때 collection 태그로 연결된 정보는 합쳐지고, 그 밖에는 id가 같은 여러개의 레코드 중에서 임의적으로 정보가 선택 저장된다.

 


그런데 pms_member_project 테이블에서 팀원 정보를 가져와서 Project 객체의 members 필드에 담아야 한다. 이 때는 findAll 태그 안에 정의한 SQL문에 이어서 outer join을 해줘야 하는데, 그 이유는 inner join을 할 경우,  pms_member_project에 팀원 정보를 저장하지 않은 프로젝트는 결과에 포함되지 않기 때문이다. 팀원이 없는 프로젝트라 할지라도 모두 결과에 담아야하므로 left outer를 사용하여 기존의 결과를 모두 포함한 상태에서 join을 해야 한다. 구체적인 SQL문은 다음과 같다.

  • 팀원의 번호를 알기 위해서 각각의 프로젝트 정보를 담은 결과와 pms_member_project 결과를 outer join해야 한다.
  • 팀원의 번호뿐만 아니라, 이름도 뽑아내기 위해서는 기존 결과와 pms_member 테이블을 또 join을 해줘야한다. 이것은 inner join이든 outer join이든 상관없다. 기존 결과에 포함된 모든 member_no 값은 pms_member에 모두 no 값으로 존재하기 때문이다.
<select id="findAll" resultMap="ProjectMap">
  select
    p.no,
    p.title,
    p.sdt,
    p.edt,
    m.no owner_no,
    m.name owner_name,
    mp.member_no,
    tm.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 tm on mp.member_no=tm.no
  order by p.no desc
</select>

이렇게 작성된 SQL 문을 직접 mysql로 전송하면 다음과 같은 결과 나온다.

+----+---------+------------+------------+----------+------------+-----------+------+
| no | title   | sdt        | edt        | owner_no | owner_name | member_no | name |
+----+---------+------------+------------+----------+------------+-----------+------+
| 11 | test200 | 2020-01-01 | 2020-02-02 |        5 | x1         |         7 | x3   |
| 11 | test200 | 2020-01-01 | 2020-02-02 |        5 | x1         |         8 | x4   |
|  8 | 4       | 2020-01-01 | 2020-10-10 |        3 | 3          |         1 | 1    |
|  8 | 4       | 2020-01-01 | 2020-10-10 |        3 | 3          |         2 | 3    |
|  7 | p2      | 2020-02-02 | 2020-03-03 |        2 | 3          |         1 | 1    |
|  7 | p2      | 2020-02-02 | 2020-03-03 |        2 | 3          |         3 | 3    |
|  6 | p1      | 2020-01-01 | 2020-02-02 |        1 | 1          |         2 | 3    |
|  6 | p1      | 2020-01-01 | 2020-02-02 |        1 | 1          |         3 | 3    |
+----+---------+------------+------------+----------+------------+-----------+------+

 

그렇다면 이 결과에 대해서 MyBatis는 어떻게 객체를 만들고 관리하는가? 일단 가장 왼쪽의 11번 프로젝트 객체를 생성하고, edt 정보까지 담은 후, owner 멤버 객체를 생성하여 프로퍼티에 넣고 나면, 7,8번 Member 객체를 생성하여 리스트에 담은 것을 Project의 members필드에 담을 것이다.

 

MyBatis가 이 과정대로 객체를 생성하여 정보를 담을 수 있도록 ProjectMap에 해당하는 코드를 수정해야 한다. 구체적인 수정 내용은 다음과 같다.

  • collection 태그로 members 연결 정보를 정의한다. property 속성값은 프로퍼티명인 members 이고,  ofType은 리스트에 담길 도메인 클래스 타입이므로 Member로 지정한다.
  • pk 값인 member_no의 연결정보는 id 태그로 설정하고, member_name은 result 태그로 설정한다. 이렇게 하면 여러개의 레코드 정보를 새로 생성한 Member 객체에 담고, 이것을 리스트로 만들어  members에 담을 것이다.
 <resultMap type="com.eomcs.pms.domain.Project"
  id="ProjectMap">
  <id 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">
   <id column="owner_no" property="no" />
   <result column="owner_name" property="name" />
  </association>

  <collection property="members"
   ofType="com.eomcs.pms.domain.Member">
   <id column="member_no" property="no" />
   <result column="member_name" property="name" />
  </collection>
 </resultMap>

 

이제 여태 Mapper에서 설정한 대로 ProjectDaoimpl 클래스에서 MyBatis를 사용하면 된다. 이미 모든 DAO 클래스에서 공유하고 있는 SqlSessionFactory를 그대로 사용하면 되기 때문에 코드는 간단하다. 

  @Override
  public List<Project> findAll() throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("ProjectDao.findAll");
    }
  }

ProjectDao - insert

insert 메서드도 해결해보자. 일단 Project의 owner 정보까지 pms_project 테이블에 insert한다. 이 insert  SQL문은 다음과 같다.

 <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>

문제는 Project의 팀원 정보들을 pms_member_project에 저장해야한다는 점이다. 이를 위한 별도의 SQL문이 필요하다.   Mapper 파일에서 새롭게 정의되는 SQL문의 구체적인 내용은 다음과 같다.

  • 해당 insert 태그의 id는 insertMember이고  parameterType은 팀원의 회원번호와, 프로젝트 번호를 둘 다 보내야 하므로 이를 Map의 형태로 받아 값을 꺼낼 수 있도록 Map으로 지정한다.
  • pms_member_project에 파라미터로 받은 팀원의 번호와 프로젝트 번호를 입력하는  SQL문을 작성한다. 
 <insert id="insertMember" parameterType="java.util.Map">
  insert into pms_member_project(member_no, project_no)
  values(#{memberNo},#{projectNo})
 </insert>

그리고 이제 ProjectDaoImpl 클래스에서 owner 정보까지는 pms_project에 담고, 팀원 정보는 반복문을 돌려 리스트에서 Member 객체를 하나씩 꺼내어 각 Member의 번호와 Project의 번호를 Map에 담아 파라미터로 넘기고 SQL문을 실행한다. 두 개의 테이블에 Insert를 하기때문에 도중에 어떤 SQL문에서 insert가 실패하더라도 데이터에 무결성을 잃지 않도록 autocommit을 false로 하고 두 테이블의 insert가 완전히 완료된 후에야 sqlSession에 대하여 commit을 호출한다.

  @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());
        map.put("projectNo", project.getNo());
        sqlSession.insert("ProjectDao.insertMember", map);
      }

      sqlSession.commit();
      return count;
    }
  }

그런데 이렇게하면 외부 키 제약 조건을 어긴 이유로 오류가 뜬다. 왜냐하면 이 project의 no값이 초기화가 되지않았으므로 0이기 떄문이다. 따라서 project 정보를 테이블에 Insert할때 자동생성되는 key 값을 알아야 한다. 

 

다음과 같이 insert 태그에 useGeneratedKeys 속성 값에 true를 지정하고, keyColumn과 keyProperty 속성 값을 자동생성된 정보가 있는 컬럼과 이 정보를 담을 프로퍼티명으로 지정해주면  MyBatis는 자동생성된 키값(keyColumn)을 파라미터로 받은 Project의 no(keyProperty)에다가 넣어준다. 따라서 이 project의 no 값을 꺼내어 pms_project_member에 insert해주면 정상적으로 정보가 들어간다.

 <insert id="insert" parameterType="com.eomcs.pms.domain.Project"
 useGeneratedKeys="true" keyColumn="no" keyProperty="no">

ProjectDao - findByNo

findByNo에 사용되는 SQL문은 findAll과 같기 때문에 일단 이 SQL문을 가져와서 수정한다. order by 대신 where 절을 넣어 하나만 리턴받도록 한다.

<select id="findByNo" parameterType="java.lang.Integer"
  resultMap="ProjectMap">
  select
  p.no,
  p.title,
  p.sdt,
  p.edt,
  m.no owner_no,
  m.name owner_name,
  mp.member_no,
  tm.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 tm on mp.member_no=tm.no
  where p.no= #{no}
  </select>

그리고 ProjectDao.findByNo() 메서드를 다음과 같이 간략하게 수정한다.

 @Override
  public Project findByNo(int no) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectOne("ProjectDao.findByNo", no);
    }
  }

ProjectDao - update

이제 ProjectDao의 update 메서드도 수정해보자. Project를 수정하려면 pms_project 테이블에 대하여 owner 정보까지만 update SQL문을 실행하여 각 컬럼값을 바꾸고, pms_member_project 테이블에 대해서는 기존에 변경할 프로젝트 번호를 갖는 레코드들을 먼저 삭제한 후 다시 새로 입력하는 편이 좋다. pms_member_project에 레코드를 입력하는 SQL문은 이미 insertMember가 있으니 새로 정의가 필요한 SQL문은 pms_project에 update를 실행하는 SQL문과 pms_member_project에 해당 프로젝트에 해당하는 레코드를 삭제하는 SQL문이다.

 

각 SQL문을 정의하는 코드는 다음과 같다.

<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="deleteMember" parameterType="java.lang.Integer">
   delete from pms_member_project 
   where project_no=#{no}
 </delete>

이렇게 두개의 SQL 문을 지정했다면, 일단 해당 프로젝트에 대해 프로젝트 테이블을 수정하는 update SQL문을 실행하고, 해당 번호의 프로젝트가 테이블에 존재하여 정상적으로 update가 이뤄졌을 경우에 한하여, deleteMember  SQL문을 실행한다. 그리고 insert 메서드에서 해줬던 것처럼 각각의 팀원 정보에 대하여 반복문을 돌려 insertMember SQL문을 실행한다. 여러번의 SQL문이 실행되었기 때문에 데이터의 무결성을 위하여 auto-commit을 false로 하고 작업이 완전히 완료되었을 때에만 commit한다.

@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.deleteMember", 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", member);
      }
      sqlSession.commit();
      return 1;
    }

ProjectDao - delete

일단 pms_project_member가 pms_project의 자식 테이블이므로 정보를 삭제하기 위해서는 먼저 pms_project_member에 입력된 팀원 정보부터 지워야 한다. 팀원 정보가 모두 지워지면, pms_project 테이블에서 해당 번호의 프로젝트 정보를 삭제하는 delete SQL문을 실행한다. pms_project_member에서 정보를 지우는 SQL문은 이미 deleteMembers로 정의되어있으므로 새로 정의가 필요한 SQL문은 pms_project에서 해당 번호의 레코드를 지우는 SQL문이다. Mapper 파일에서 이 SQL문을 정의하는 코드는 다음과 같다.

 <delete id="delete" parameterType="java.lang.Integer">
  delete from pms_project
  where no=#{no}
 </delete>

이제 ProjectDaoImpl에서 deleteMembers를 실행한 후 delete을 실행하며, 여러 번의 SQL문을 실행하였으므로 auto-commit은 false로 지정하고, 모든 작업이 완료되는 시점에서 commit을 한다.

  @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;
    }
  }

그런데 한가지 문제될 점은 pms_task에서 만약 해당 프로젝트에 대해 작업을 담당하고 있는 팀원의 정보가 있을 경우, 이것을 처리하지 않고, Project 정보를 지우려고 하면 외부키 제약 조건을 어겨 예외가 발생한다는 점이다. 따라서 프로젝트를 참조하고 있는 작업 정보를 먼저 삭제 해야만 프로젝트 정보도 삭제할 수가 있는 데 이것은 또다른 문제를 낳는다. 작업정보를 지우는 일은 taskDao과 관여할 일로 ProjectDeleteCommand에서 projectDao의 delete 메서드와 taskDao의 delete 메서드를 둘 다 호출해야하는데, 이때 각각의 다른 SqlSession으로 작업을 수행하기 때문에 둘 다 오토커밋을 true로 해야 하므로 특정 테이블의 정보를 삭제하는 과정에서 작업 실패하면 정보의 무결성이 지키지 못하게 된다. 그것을 해결하기 위한 방법은 추후에 배울 것이다.