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

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

MyBatis 문법

com.eomcs.mybatis.ex01

Data Persistence Framework

데이터의 영속성(지속성; 등록, 조회, 변경, 삭제)를 대신 처리해주는 프레임워크를 말한다. 퍼시스턴스 프레임워크를 사용하면 JDBC의 복잡함이나 번거로움 없이 간단한 작업만으로 데이터 베이스와 연동되는 시스템을 빠르게 개발할 수 있으며 안정적인 구동도 보장한다. Data Persistence Framework에는 SQL문장으로 직접 데이터베이스 데이터를 다루는 SQL Mapper와, 자바 객체를 통해 간접적으로 데이터베이스 데이터를 다루는 OR Mapper(Object-Relational mapper; 객체 관계 맵퍼)가 있다.

컴퓨터 공학에서 지속성(Persistance)는 프로세스가 생성했지만 별개로 유지되는 상태의 특징 중 한 가지로, 별도의 기억 장치에 데이터를 보존하는 것을 목적으로 한다.

SQL Mapper는 직접 SQL문을 작성한다. 따라서 각각의 DBMS에 최적화된 SQL을 작성할 수 있다. 그러나 DBMS마다 미미하게 다른 SQL을 작성해야 하는 번거로움이 있다. Mybatis가 대표적으로 다른 SQL Mapper는 거의 사용되지 않는다. 주로 SI 업체에서 사용한다.

OR Mapper는 전용 언어 및 문법(Domain-Specific Language; DSL)을 사용하여 작성하고, 실행할 때 DBMS에 맞춰서 SLQ을 생성하여 실행한다. DBMS마다 SLQ문을 작성할 필요가 없어 편리하지만 DBMS에 최적화된 SQL을 실행할 수 없다는 단점이 있다. 즉 DBMS에 최적화된 SQL을 실행할 수 없다. Hibernate, TopLink가 대표적으로, 주로 서비스 업체에서 사용한다.

MyBatis 도입

1. 의존 라이브러리 추가

  • build.gradle 파일에 의존 라이브러리 추가 후 터미널에서 gradle eclipse 명령을 실행한다.

2. mybatis 설정 파일 준비

com.eomcs.mybatis.ex1.mybatis-config.xml

<properties>resource 속성값으로 적은 경로에 있는 jdbc.properties 파일의 내용을 읽어온다. 읽어온 정보는 ${프로퍼티명} 문법을 이용하여 그 값을 사용할 수 있다.클래스 파일이 아니기 때문에 .를 사용하는 것이 아니라 /를 사용하여 일반 파일 경로를 적어준다.

1
<properties resource="com/eomcs/mybatis/ex01/jdbc.properties"></properties>

<environments> 태그에서는 DBMS에 연결할 때 사용할 정보를 설정한다. 열어 개의 연결 정보를 설정해두고 그 중에 사용할 정보를 지정할 수 있다. defaul="development"의 의미는, 여러 연결 정보 중에서 development라는 연결 정보를 사용하여 실행하겠다는 의미이다.

1
<environments default="development">

각각의 연결 정보는 다음과 같이 <environment> 태그에 설정한다.

1
<environment id="development">

트랜젝션 관리 방식을 지정한다. 커넥션 객체에 대해서 대신 commit, rollback 한다는 의미이다.

1
<transactionManager type="JDBC">

<dataSource>에서는 DB 커넥션 풀에 관련된 정보와 DB 연결 정보를 설정한다. 이제 개발자가 DB 커넥션 풀을 다룰 필요가 없다. mybatis 프레임워크에서 관리한다.

로컬에서는 DB 커넥션 풀이 필요 없다. 이것을 서버로 옮기면 DB 커넥션 풀이 필요할 것이다.

DB Connection Pool? DB와 미리 connection(연결)을 해놓은 객체들을 pool(웅덩이)에 저장해두었다가, 클라이언트 요청이 오면 커넥션을 빌려주고, 볼 일이 끝나면 다시 커넥션을 반납받아 pool에 저장하는 방식을 말한다.

1
2
3
4
5
6
7
<dataSource type="POOLED">
  <!-- ${위의 .properties 파일에 저장된 프로퍼티명} -->
  <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>

<mappers><mapper>에는 SQL문을 모아둔 파일(SQL Mapper 파일)을 지정한다. SQL Mapper 파일에 작성해둔 SQL 문을 mybatis가 사용할 것이다. 맵퍼 파일의 경로를 지정할 때 classpath 경로를 사용해야 한다. 단 패키지명을 구분할 때 . 대신에 /를 사용해야 한다. 클래스 파일이 아니라 일반 파일이기 때문이다.

1
2
3
  <mappers>
    <mapper resource="com/eomcs/mybatis/ex01/BoardMapper.xml"/>
  </mappers>

3. DB 연결 정보를 담은 프로퍼티 파일 준비

jdbc.properties

1
2
3
4
5
# key=value
jdbc.driver=org.mariadb.jdbc.Driver
jdbc.url=jdbc:mariadb://localhost:3306/studydb
jdbc.username=study
jdbc.password=1111

4. SQL 문장을 작성할 파일

BoardMapper.xml

SQL 문장을 찾을 때 사용할 그룹명을 설정한다. 보통 그룹명은 SLQ Mapper 파일이나 그 파일이 있는 경로를 그룹명으로 지정한다. 또는 SQL을 사용할 인터페이스나 클래스 경로를 그룹명으로 지정한다. 이것은 관습적이기 때문에 실제로는 어떤 이름으로 지정해도 상관 없지만, 가능한 규칙을 준수하여 유지보수의 일관성을 유지하는 것이 좋다.

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
<?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="BoardMapper">
  
  <select id="selectBoard" resultType="com.eomcs.mybatis.ex01.Board">
    select 
      board_id,
      title,
      contents,
      created_date,
      view_count
    from x_board
  </select>
  
  <select id="selectBoard2" resultType="com.eomcs.mybatis.ex01.Board">
    select 
      board_id as no,
      title,
      contents as content,
      created_date registeredDate,
      view_count viewCount
    from x_board
  </select>
</mapper>

5. Mybatis 객체 준비

5.1. mybatis 설정 파일을 읽을 InputStream 도구 준비

5.1.3. 직접 파일 시스템 경로 지정

단 소스 파일 경로를 지정하는 것이 아니라 컴파일된 후 XML 파일이 놓이는 경로를 지정해야 한다. 자바 패키지에 작성일반 파일은 그대로 빌드 디렉토리에 복사된다.

1
2
InputStream mybatisConfigInputStream = new FileInputStream(
        "./bin/main/com/eomcs/mybatis/ex01/mybatis-config.xml");

그러나 mybatis 설정 파일의 경로를 직접 지정하면 애플리케이션 배포 경로가 바뀔 때마다 소스를 변경하고 다시 컴파일해야 하는 문제가 있다.

5.1.2. Resources 클래스의 메서드 이용

이를 간편하게 하기 위해 Mybatis는 Resources라는 도우미 객체를 제공한다. 이 클래스의 메서드를 이용하면 자바 클래스가 있는 패키지 폴더에서 mybatis 설정 파일을 찾을 수 있다.

1
2
InputStream mybatisConfigInputStream = Resources.getResourceAsStream(
  "com/eomcs/mybatis/ex01/mybatis-config.xml");

파라미터에 mybatis 설정 파일의 경로를 지정할 때, 자바 패키지 경로를 그대로 이용한다. 단 파일 경로이기 때문에 폴더와 폴더 사이를 가리킬 때 . 대신에 /를 사용해야 한다. JVM은 현재 실행하는 애플리케이션의 자바 클래스 경로를 알고 있다.

자바 패키지 경로에서 찾기 때문에 mybatis 설정 파일은 반드시 자바 패키지 경로에 있어야 한다.

5.2. SqlSessionFactory를 만들어 줄 빌더 객체 준비
1
SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
5.3. SqlSession 객체를 만들어줄 팩토리 객체 준비

mybatis 는 Builder를 이용하여 SqlSessionFactory 객체를 만든다. 이때 공장 객체를 만들 때 사용할 설정 파일을 지정한다. 설정 파일의 경로를 직접 지정하지 말고, 해당 파일을 읽을 때 사용할 InputStream을 넘겨준다.

1
SqlSessionFactory factory = factoryBuilder.build(mybatisConfigInputStream);
5.4. SQL을 실행시키는 객체 준비
1
SqlSession sqlSession = factory.openSession();

SqlSessionFactory 객체로부터 SqlSession 객체를 얻는다. openSession()은 수동 커밋으로 SQL을 다루는 객체를 리턴한다. 자동 커밋으로 SQL을 다루려면 openSession(boolean autoCommit) 메서드를 호출해야 한다.

Builder는 한 번만 사용하기 때문에 다음과 같이 코드를 정리할 수 있다.

1
2
3
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(
  Resources.getResourceAsStream(
    "com/eomcs/mybatis/ex01/mybatis-config.xml"));

SQL 문 실행

SqlSession 객체를 이용하여 SQL 맵퍼 파일에 작성한 SQL문을 실행한다.

  • select 문장
    • sqlSession.selectList(): 목록 리턴
    • sqlSession.selectOne(): 한 개의 결과 리턴
  • insert 문장
    • sqlSession.insert()
  • update 문장
    • sqlSession.update()
  • delete 문장
    • sqlSession.delete()

insert, update, delete인 경우 insert(), update(), delete() 메서드 중 아무거나 호출해도 괜찮다. 그러나 일관된 유지보수를 위해 메서드를 구분해서 사용하는 것이 권장된다.

메서드를 사용하기 위해서는 SQL식별자와 파라미터값을 넘겨주어야 한다. SQL 식별자는 그룹명+'.'+SQL문장아이디를 뜻한다. 여기서 그룹명은 <mapper namesace="그룹명">..<mapper>에서 확인할 수 있다. 파라미터 값으로는 원시타입 및 모든 자바 객체가 가능하다. 여러 개의 값을 전달할 때는 Map에 담아 넘기도록 하자.

컬럼 이름과 프로퍼티의 이름

selectList()의 동작 원리

1
2
3
4
5
6
7
8
9
10
11
List<Board> list = 
  sqlSession.selectList("BoardMapper.selectBoard");

for (Board board : list) {
  System.out.printf("%d, %s, %s, %s, %d\n", 
                    board.getNo(), 
                    board.getTitle(), 
                    board.getContent(),
                    board.getRegisteredDate(),
                    board.getViewCount());
}
1
2
3
4
5
6
7
8
9
<select id="selectBoard" resultType="com.eomcs.mybatis.ex01.Board">
select 
  board_id,
  title,
  contents,
  created_date,
  view_count
from x_board
</select>

resultType에 지정한 클래스의 인스턴스를 생성한다. 컬럼 이름과 일치하는 프로퍼티를 찾아 값을 입력한다. board.idsetBoard_id(컬럼값)을, titlesetTitle(컬럼값)을, contentssetContents(컬럼값)을, created_datesetCreated_date(컬럼값)을, view_countsetView_count(컬럼값)을 호출한다. 컬럼 이름과 일치하는 프로퍼티(setter)가 없다면 그 컬럼의 값은 객체에 담을 수 없다. 이 예제에서 컬럼 이름과 일치하는 프로퍼티는 title밖에 없다.

컬럼 이름과 자바 객체의 프로퍼티 이름 일치시키기

럼의 값을 자바 객체에 담으려면 컬럼과 같은 이름의 프로퍼티가 있어야 한다.

1
2
3
4
5
List<Board> list = sqlSession.selectList("BoardMapper.selectBoard2");

for (Board board : list) {
  System.out.printf("%d, %s, %s, %s, %d\n", board.getNo(), board.getTitle(), board.getContent(), board.getRegisteredDate(), board.getViewCount());
}

없다면 프로퍼티 명을 다음과 같이 컬럼의 별명으로 지정한다.

1
2
3
4
5
6
7
8
9
<select id="selectBoard" resultType="com.eomcs.mybatis.ex01.Board">
 select
   board_id as no,
   title,
   contents as content,
   created_date as registeredDate,
   view_count as viewCount
 from x_board
</select>

별명을 붙일 때는 as를 붙여도 되고 붙이지 않아도 된다.

클래스 별명 지정하기

<typeAliase> 태그를 사용하면 mybatis 설정 파일에서 fully-qualified class name을 사용하는 대신에 짧은 이름으로 대체할 수 있다. 이때 패키지를 포함한 클래스 이름은 항상 . 으로 표기해야 한다. / 는 파일 경로를 가리킬 때 사용한다. <properties><settings> 뒤에 <typeAliases>를 적는다. 만약 이 순서를 지키지 않는다면 에러가 뜰 것이다.

1
2
3
<typeAliases>
  <typeAlias type="com.eomcs.mybaits.ex1.Board" alias="abc"></typeAlias>
</typeAliases>

이 별명은 다음과 같이 SQL 맵퍼 파일에서 클래스를 지정할 때 사용한다. 별명은 대소문자를 구분하지 않는다.

1
2
3
4
5
6
7
8
9
<select id="selectBoard2" resultType="abc">
 select
	board_id as no,
 	title,
	contents as content,
  created_date registeredDate,
  view_count viewCount
 from x_board
</select>

패키지에 소속된 전체 클래스에 대해서도 별명을 부여할 수 있다.

1
2
3
<typeAliases>
  <package name="com.eomcs.mybastis.ex01"/>
</typeAliases>

SQL문은 태그 안에 작성한다. <select> 태그에는 select 문장을, <insert> 태그에는 insert 문장을, <update> 태그에는 update 문장을, <delete> 태그에는 delete 문장을 작성한다. 그런데 insert/update/delete 인 경우 <insert>/<update>/<delete> 구분없이 태그를 사용해도 된다. 그 이유는 SQL문을 찾을 때 id 속성 값으로 찾기 때문이다. 그럼에도 불구하고 유지보수의 일관성을 위해 SQL 문의 따라 적절한 태그를 사용하라!

select 컬럼과 프로퍼티

  • id: SQL문을 찾을 때 사용할 식별자이다.
  • resultType: select 결과를 저장할 클래스 이름이나 별명이다. 클래스 이름일 경우 반드시 fullly-qualified class name(패키지명을 포함한 클래스명)을 사용해야 한다.

값을 자바 객체에 넣는 규칙

  • 컬럼명과 일치하는 setter를 호출한다.

  • 컬럼명 => set컬럼명()

    1
    2
    3
    
    //예:
    Board board = new Board();
    board.setBno(rs.getNo("bno"));
    
  • 만약 컬럼 이름에 해당하는 셋터를 못 찾으면 호출하지 않는다.

1
2
3
4
5
6
7
8
9
<select id="selectBoard" resultType="Board">
  select 
  board_id, <!-- Board.setBoard_id() 호출 -->
  title,    <!-- Board.setTitle() 호출 -->
  contents, <!-- Board.setContents() 호출 -->
  created_date, <!-- Board.setCreated_date() 호출 -->
  view_count    <!-- Board.setView_count() 호출 -->
  from x_board
</select>

위의 SQL문을 mybatis는 내부에서 다음과 같은 코드로 실행할 것이다.

1
2
3
4
5
6
7
8
9
10
while (rs.next()) {
  Board board = new Board();
  board.setBoard_id(rs.getNo("board_id")); // 이런 셋터가 없다.
  board.setTitle(rs.getString("title")); // 이 셋터는 있다.
  board.setContents(rs.getString("contents")); // 이런 셋터가 없다.
  board.setCreated_date(rs.getDate("created_date")); // 이런 셋터가 없다.
  board.setView_count(rs.getDate("view_count")); // 이런 셋터가 없다.
  list.add(board);
} 
return list;

그러나 Board 클래스에는 컬럼 이름과 일치하는 셋터가 딱 한 개만 있다. title 컬럼이다. 그 외 컬럼 값은 셋터가 없기 때문에 저장할 수 없다. 즉 mybatis에서 결과 값을 Board 객체에 담지 못한다. 따라서 다음 코드는 실행 오류가 발생한다.

1
2
3
4
5
6
7
8
9
List<Board> list = sqlSession.selectList("BoardMapper.selectBoard");

for (Board board : list) {
  System.out.printf("%d, %s, %s, %s\n", //
                    board.getNo(), //
                    board.getTitle(), //
                    board.getContent(), //
                    board.getRegisteredDate());
}

이를 해결하기 위해서는 셋터의 이름(프로퍼티 이름)과 같은 이름으로 컬럼의 별명을 설정해야 한다. 즉, 컬럼 이름을 프로퍼티 이름과 일치시킴으로써 setter를 정확하게 호출하도록 만든다.

1
2
3
4
5
6
7
8
9
<select id="selectBoard" resultType="Board">
  select 
  board_id as no,     <!-- Board.setNo() 호출 -->
  title,              <!-- Board.setTitle() 호출 -->
  contents content,   <!-- Board.setContent() 호출 -->
  created_date as registeredDate, <!-- Board.setRegisteredDate() 호출 -->
  view_count viewCount            <!-- Board.setViewCount() 호출 -->
  from x_board
</select>

그러나 이렇게 select를 할 때마다 컬럼명에 일일이 별명을 붙이는 것은 귀찮은 일이다. 이를 손쉽게 해주는 <resultMap> 태그가 있다. 이 태그가 하는 일은 컬럼명과 자바 객체의 프로퍼티 명을 미리 연결하는 것이다. <resultMap>type 속성에는 자바 객체의 클래스명 혹은 별명을 넣고, id에는 연결 정보를 가리키는 식별자를 지정한다. <result> 태그의 column 속성에는 컬럼명을, property 속성에는 자바 객체의 프로퍼티명을 지정한다. 단, primary key 컬럼인 경우 <result> 태그 대신 <id> 태그를 사용한다. id 로 지정된 컬럼값이 같을 때는 같은 값으로 취급한다. 이때, 컬럼명과 프로퍼티명이 같을 때는 <result>로 지정하지 않아도 된다.

1
2
3
4
5
6
7
8
<resultMap type="Board" id="BoardMap">
  <id column="board_id" property="no"/>
  <!--  컬럼명과 프로퍼티명이 같을 때는 result 로 지정하지 않아도 된다. -->
  <!-- <result column="title" property="title"/> -->
  <result column="contents" property="content"/>
  <result column="created_date" property="registeredDate"/>
  <result column="view_count" property="viewCount"/>
</resultMap>

위에서 정의한 연결 정보를 사용하고 싶다면, resultMap="컬럼과 프로퍼티의 연결을 정의한 resultMap 아이디"를 설정하자.

1
2
3
4
5
6
7
8
9
  <select id="selectBoard" resultMap="BoardMap">
    select 
      board_id, <!-- BoardMap의 연결정보를 참조하기 때문에 별명을 주지 않아도 된다. -->
      title, 
      contents, 
      created_date,
      view_count 
    from x_board
  </select>

만약 setter()에 접근 제어자를 걸었을 때는 어떻게 될까? 이게 궁금해서 기존의 bitcamp-java-project의 Board 클래스에 있는 setTitle() 메서드를 private으로 접근제어자를 걸어 두고, 실행문 안에 "setTitle()"이라는 문자열을 출력하는 코드를 두어, setTitle()이 호출되는지 확인하고자 하였다.

1
2
3
4
private void setTitle(String title) {
    System.out.println("setTitle()");
    this.title = title;
  }

실행 결과는 다음과 같았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
번호, 제목, 작성자, 등록일, 조회수
setTitle()
setTitle()
setTitle()
setTitle()
setTitle()
setTitle()
8, this is from hayeon, hayeon, 2020-11-11, 3
7, dkdkd, hayeon, 2020-11-06, 0
6, title, hayeon, 2020-11-06, 0
5, haha, hayeon, 2020-11-05, 0
2, 222, hayeon, 2020-11-03, 0
1, hihi, hayeon, 2020-11-03, 2

private으로 setter를 설정해도 mybatis는 setTitle() 메서드를 호출할 수 있었다. 이게 어떻게 가능할까? 강사님께서는 아마 mybatis는 내부적으로 Reflection API를 사용할 것이라고 하셨다. Reflection API에는 메서드 객체를 얻을 수 있는 getDeclaredMethod() 메서드가 있다. (getMethod()public 메서드만을 가져오는 반면, getDeclaredMethod()는 접근제어자가 어떤 것이든 모두 가져온다.) 이를 Method 객체로 가져와 invoke() 메서드를 호출하면 해당 메서드를 호출할 수 있다.

다만, 접근제어자가 public이 아닌 경우setAccessible(true)로 설정을 함으로써 접근할 수 있도록 강제로 설정하는 작업을 해줘야지 정상적으로 invoke()할 수 있다.

Reflection API를 사용하면 접근제어자가 걸리든 말든 접근할 수 있다니.. 게다가 특정 메서드를 직접 호출하는 것이 아니라 Method 객체에 담아 invoke()로 호출할 수 있다는 것도 상당히 충격이었다. 강사님께서는 일부 개발자들은 Reflection API의 이러한 특징 때문에 Reflection API가 객체지향을 무너뜨린다고들 하며 꺼려한다고 말씀하셨다. (그럴 만도 하다)

아무튼 종합해보면 우리는 mybatis가 다음 코드처럼 돌아갈 거라고 짐작할 수 있다.

1
2
3
4
Board b = new Board();
Method m = Board.class.getDeclaredMethod("setTitle", String.class);
m.setAccessible(true);
m.invoke(b, "오호라!");

SQL에 파라미터 지정하기

SQL을 실행할 때 파라미터 값을 전달하려면 두 번째 파라미터로 전달해야 한다. 여러 개의 값을 전달해야 한다면, Map 객체나 도메인 객체에 담아 전달한다.

값 한 개를 넘길 때

Integer 객체를 파라미터로 넘긴다. 아래 코드에서 5가 그대로 넘어가는 것이 아니라 auto-wrapping 되어서 넘어간다. => Integer.valueOf(5)

1
2
List<Board> list = sqlSession.selectList(
  "BoardMapper.selectBoard1", 5);

따라서 parameterType을 _int(원시타입)이 아니라 int(Integer 타입)로 지정해도 되는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- selectList(sqlid, int) --> 
<select id="selectBoard1" 
        resultMap="BoardMap" 
        parameterType="int">
  select 
    board_id,
    title, 
    contents, 
    created_date,
    view_count 
  from x_board
  where board_id > #{ohora}
</select>

이때 where 절에 있는 ><로 바꾸면 어떻게 될까? 오류가 발생한다. XML Parser가 < 기호를 만나면 태그가 시작하는 줄 알고 xml 문법 오류를 발생시킨다. 따라서 해당 구문을 <![CDATA[]]>로 감싸서 parser에게 이것은 태그가 아니라 문자열이라고 알려줘야 한다.

xml parser는 xml의 태그를 찾아 읽어서 처리하는 역할을 한다.

1
2
3
4
5
6
7
8
9
10
11
    <![CDATA[
    select 
      board_id,
      title, 
      contents, 
      created_date,
      view_count 
    from x_board
    where board_id < #{ohora}
    ]]>
  </select>

혹은 < 기호 대신 그 자리에 &lt;(less than)라는 XML 상수값을 사용할 수도 있다. 그러나 이렇게 표현하면 다른 개발자가 알아보기 어려우니 그냥 <![CDATA[]]>로 감싸는 것이 권장된다.

1
2
3
4
5
6
7
8
9
10
11
12
  <select id="selectBoard1" 
          resultMap="BoardMap" 
          parameterType="int">
    select 
      board_id,
      title, 
      contents, 
      created_date,
      view_count 
    from x_board
    where board_id &lt; #{ohora}
  </select>

문자열을 파라미터로 넘긴다. 다음은 게시글 제목에 ohora를 포함한 게시글을 찾는 코드이다.

1
2
List<Board> list = sqlSession.selectList(
  "BoardMapper.selectBoard2", "%ohora%");
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- selectList(sqlid, String) -->
<select id="selectBoard2" 
        resultMap="BoardMap" 
        parameterType="string">
  select 
    board_id,
    title, 
    contents, 
    created_date,
    view_count 
  from x_board
  where title like #{haha}
</select>

Map에 값을 담아 넘기기

다음은 페이징 처리를 위한 시작 인덱스 개수를 파라미터로 전달하는 코드이다.

1
2
3
4
5
6
HashMap<String, Object> params = new HashMap<>();
params.put("startIndex", 6);
params.put("size", 3);

List<Board> list = sqlSession.selectList(
  "BoardMapper.selectBoard3", params);

Map에서 값을 꺼낼 때는 값을 저장할 때 지정한 key를 이용한다. => #{key}

1
2
3
4
5
6
7
8
9
10
11
12
<select id="selectBoard3" 
        resultMap="BoardMap" 
        parameterType="map">
  select 
  board_id,
  title, 
  contents, 
  created_date,
  view_count 
  from x_board
  limit #{startIndex}, #{size}
</select>

만약 페이지에 보여주는 게시물 개수(size)가 3개면, 9가 4번째 페이지의 시작 인덱스이다. 즉 3번째까지의 게시물 개수(3 x 3)가 4번째 페이지의 시작 인덱스이다. 즉 페이지의 시작 인덱스는 (이전까지의 페이지 개수(startIndex - 1) x 보이는 게시글 개수(size) = 이전까지의 게시물 개수)이다.

#{}의 한계와 ${}의 쓰임새

#{}는 값을 삽입할 때 사용하고, ${}는 SQL문을 삽입할 때 사용한다. 다음은 파라미터로 컬럼 이름을 넘겨주면 해당 컬럼의 값을 오름차순으로 정렬하는 코드이다.

1
2
List<Board> list = sqlSession.selectList(//
  "BoardMapper.selectBoard1", "title");

파라미터 값을 SQL에 그대로 삽입하려면 #{} 문법을 사용해서는 안된다. ${} 문법을 사용해야 한다. #{}은 값을 넣을 때 사용한다. 다음과 같이 #{}를 사용하면 order by가 정상적으로 적용되지 않는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="selectBoard1" 
        resultMap="BoardMap" 
        parameterType="string">
  select 
    board_id,
    title, 
    contents, 
    created_date,
    view_count 
  from x_board
  order by #{colname} asc
  <!-- #{}는 값을 삽입할 때 사용하고, 
        ${}는 SQL문을 삽입할 때 사용한다. -->
</select>

위 문장에 파라미터 값을 넣으면 order by 'title'과 같다. MariaDB는 order by 뒤에 문자열이 오면 다음과 같이 order by 자체를 무시한다.

1
2
3
4
5
6
7
8
9
10
11
12
MariaDB [studydb]> select * from x_board order by 'okok' desc;
+----------+--------------+--------------+---------------------+------------+
| board_id | title        | contents     | created_date        | view_count |
+----------+--------------+--------------+---------------------+------------+
|        1 | 제목1        | 내용         | 2020-11-11 10:53:19 |          0 |
|        2 | 제목2        | 내용         | 2020-11-11 10:53:19 |          0 |
|        3 | 제목3        | 내용         | 2020-11-11 10:53:19 |          0 |
|        4 | 제목4        | 내용         | 2020-11-11 10:53:19 |          0 |
|        5 | 제목5        | 내용         | 2020-11-11 10:53:19 |          0 |
|        6 | 제목6        | 내용         | 2020-11-11 10:53:19 |          0 |
|        7 | 제목이래요!4 | 내용이래요!4 | 2020-11-11 13:49:06 |          0 |
+----------+--------------+--------------+---------------------+------------+

다시 말해, #{}는 값을 삽입할 때 사용하고, ${}는 SQL문을 삽입할 때 사용한다. 따라서 다음과 같이 ${}를 사용하도록 바꾸어주었다.

1
2
3
4
5
6
7
8
9
10
11
12
<select id="selectBoard2" 
        resultMap="BoardMap" 
        parameterType="string">
  select 
    board_id,
    title, 
    contents, 
    created_date,
    view_count 
  from x_board
  order by ${colname} asc
</select>

${}를 사용하면 다음과 같이 mybatis에 SQL문을 만들어 전달할 수 있다.

1
2
List<Board> list = sqlSession.selectList(//
  "BoardMapper.selectBoard3", "where title like '%oh%'");
1
2
3
4
5
6
7
8
9
10
11
<select id="selectBoard3" 
        resultMap="BoardMap" parameterType="string">
  select 
    board_id,
    title, 
    contents, 
    created_date,
    view_count 
  from x_board
  ${sql}
</select>

SQL Mapper에서는 ${}문법으로 SQL문을 받는다. 단, 사용자가 입력한 값을 그대로 전달한다면, SQL 삽입 공격에 노출될 수 있기 때문에 위험하다. 사용자가 입력한 값을 그대로 전달하지 않고, 개발자가 지정한 값을 전달한다면 안전하게 사용할 수 있다.

SQL에 파라미터로 일반 객체 전달하기

Board 객체에 값을 저장하여 전달할 수 있다. 단, 값을 꺼낼 수 있도록 겟터(프로퍼티)가 있어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Board board = new Board();
board.setTitle("제목");
board.setContent("내용");

System.out.printf("번호: %d\n", board.getNo());     //0
System.out.printf("제목: %s\n", board.getTitle());  //제목
System.out.printf("내용: %s\n", board.getContent());//내용
System.out.println("-------------------------------------");

int count = sqlSession.insert("BoardMapper.insertBoard", board);
System.out.println(count);

// Mybatis가 Board 객체의 내용을 테이블에 저장한 뒤에도
// Board 객체의 번호는 계속 0인 채로 있다.
System.out.printf("번호: %d\n", board.getNo());     //0
System.out.printf("제목: %s\n", board.getTitle());  //제목
System.out.printf("내용: %s\n", board.getContent());//내용

sqlSession.commit();

sqlSession.close();

mybatis에서는 autocommit이 기본으로 false이다. autocommit은 insert/update/delete와 같이 데이터를 변경하는 작업은 클라이언트에서 최종적으로 변경을 허락해야만 진짜 테이블에 값을 반영한다. mybatis에서는 다음 메서드를 호출하여 DBMS에 작업 결과를 진짜 테이블에 반영하라고 명령해야 한다. commit 명령을 내리지 않으면 insert/update/delete를 테이블에 반영하지 않는다. close()할 때 취소된다.

  • commit: 임시 메모리에 저장된 작업 결과를 실제 테이블에 반영시키는 명령
  • rollback: 임시 메모리에 저장된 작업 결과를 취소하는 명령

INSERT/UPDATE/DELETE

Insert SQL 실행하기

1
2
3
4
5
  <!-- insert(sqlId, Board) -->
  <insert id="insertBoard" parameterType="Board">
    insert into x_board(title,contents,created_date) 
    values(#{title},#{content},now())
  </insert>  

INSERT를 실행한 후 자동으로 생성된 PK 값을 가져오기 위해서는 다음과 같이 파라미터로 넘겨준 객체에 담아 달라고 요청해야 한다.

1
2
3
4
5
<insert id="insertBoard" parameterType="Board"
        useGeneratedKeys="true" keyColumn="board_id" keyProperty="no">
  insert into x_board(title,contents,created_date) 
  values(#{title},#{content},now())
</insert>  

mybatis는 insert를 실행한 후자동 증가된 PK 값(board_id 컬럼의 값)을 도로 board 객체에 담아 줄 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Board board = new Board();
board.setTitle("제목2");
board.setContent("내용2");

System.out.printf("번호: %d\n", board.getNo());     //0
System.out.printf("제목: %s\n", board.getTitle());  //제목2
System.out.printf("내용: %s\n", board.getContent());//내용2
System.out.println("-----------------------------------");

int count = sqlSession.insert("BoardMapper.insertBoard", board);
System.out.println(count);

System.out.printf("번호: %d\n", board.getNo());    //자동증가된값
System.out.printf("제목: %s\n", board.getTitle()); //제목2
System.out.printf("내용: %s\n", board.getContent());//내용2

sqlSession.commit();

sqlSession.close();

update SQL 실행하기

BoardMapper08

update SQL을 실행할 때 update 태그 대신에 insert/delete 태그를 사용해도 된다. mybatis는 SQL을 찾을 id 값으로 찾기 때문이다. 그러나 유지보수를 위해 가능한 일관된 이름을 사용하는 것이 좋다. 즉 insert SQL문은 insert 태그에 넣고, update SQL문은 update 태그에 넣자.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 변경할 데이터를 객체에 담아서 넘긴다.
Board board = new Board();
board.setNo(5);
board.setTitle("aaaa");
board.setContent("bbbb");

int count = sqlSession.update("BoardMapper.updateBoard", board);
System.out.println(count);
sqlSession.commit();
// commit 명령을 내리지 않으면 insert/update/delete을 테이블에 반영하지 않는다.
// close() 할 때 취소된다.

sqlSession.close();
1
2
3
4
5
6
<insert id="updateBoard" parameterType="Board">
  update x_board set
    title=#{title},
    contents=#{content}
  where board_id=#{no}
</insert>  

Delete SQL 실행하기

Exam0260

delete SQL 실행할 때는 먼저 자식 테이블의 데이터를 지우고, 부모 테이블의 데이터를 지워야 한다. foreign 키가 걸려 있기 때문이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SqlSession sqlSession = factory.openSession();

// 먼저 자식 테이블의 데이터를 지운다.
int count = sqlSession.delete("BoardMapper.deleteBoardFile", 3);
System.out.println(count);

// 그런 후 부모 테이블의 데이터를 지운다.
count = sqlSession.delete("BoardMapper.deleteBoard", 3);
System.out.println(count);

sqlSession.commit();
// commit 명령을 내리지 않으면 insert/update/delete을 테이블에 반영하지 않는다.
// close() 할 때 취소된다.

sqlSession.close();
1
2
3
4
5
6
7
8
9
<delete id="deleteBoard" parameterType="int">
  delete from x_board
  where board_id=#{value}
</delete>

<delete id="deleteBoardFile" parameterType="int">
  delete from x_board_file
  where board_id=#{value}
</delete>  

dynamic SQL

dynamic SQL은 조건에 따라 SQL을 달리 생성하는 것을 말한다. mybatis는 이를 위해 조건에 따라 SQL문을 변경하거나, 동일한 SQL을 반복적으로 생성할 수 있는 문법을 제공한다.

<if>: 조건 설정

사용자로부터 게시글의 번호를 입력 받아 조회하고, 만약 오류가 발생하면 전체 게시글을 출력하는 프로그램이 있다. 만약 dynamic SQL을 사용하지 않는다면, 조건에 따라 게시글 번호가 주어지면 특정 게시글만 조회하는 SQL(BoardMapper.select1)을 실행하고, 게시글 번호가 없으면 전체 게시글을 조회하는 SQL(BoardMapper.select2)을 실행하는 코드를 다음과 같이 자바 코드로 작성해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Scanner keyScan = new Scanner(System.in);
System.out.print("게시글 번호? ");
String str = keyScan.nextLine();
keyScan.close();

List<Board> list = null;

try {
  // => 게시글 번호가 주어지면 특정 게시글만 조회하는 select1 SQL을 실행
  list = sqlSession.selectList("BoardMapper.select1", Integer.parseInt(str));

} catch (Exception e) {
  // => 오류가 나면 전체 게시글을 조회하는 select2 SQL을 실행
  list = sqlSession.selectList("BoardMapper.select2");
}

for (Board board : list) {
  System.out.printf("%d, %s, %s, %d\n", //
                    board.getNo(), //
                    board.getTitle(), //
                    board.getRegisteredDate(), //
                    board.getViewCount());
}
1
2
3
4
5
6
7
8
9
10
11
12
<select id="select1" resultMap="BoardMap" parameterType="int">
  select 
    board_id, title, contents, created_date, view_count 
  from x_board
  where board_id = #{value}
</select>

<select id="select2" resultMap="BoardMap" parameterType="int">
  select 
    board_id, title, contents, created_date, view_count 
  from x_board
</select>

dynamic SQL을 사용해 다음과 같이 코드를 변경할 수 있다. <if> 조건문을 사용해 조건에 따라 생성할 SQL문을 제어할 수 있다. => <if test="조건">SQL문</if>

Parameter 타입이 만약 사용자 지정 타입 (ex: Board)이라면 #{}안에 정확히 주어야 한다. 반면 래퍼 클래스 등이라면 아무거나 줘도 된다!

1
2
3
4
5
6
7
8
9
<select id="select3" resultMap="BoardMap" parameterType="int">
  select 
    board_id, title, contents, created_date, view_count 
  from x_board
  <if test="no != null">
    where board_id = #{no}
  </if>
</select>

이제 각 조건에 따라 다른 SQL문의 id를 지정하는 것이 아니라, 같은 SQL문(select3)을 실행한다. Mapper 파일에서 조건으로 SQL을 제어한다. 조건에 따라 여러 개의 SQL을 생성할 필요가 없어 편리하다.

1
2
3
4
5
6
7
try {
  // 게시글 번호가 주어지면 해당 게시글만 출력한다.
  list = sqlSession.selectList("BoardMapper.select3", Integer.parseInt(str));
} catch (Exception e) {
  // 게시글 번호가 없거나 예외가 발생하면 전체 게시글을 출력한다.
  list = sqlSession.selectList("BoardMapper.select3");
}

${}를 사용해 직접 SQL문을 넘겨줌으로써 하나의 SQL문만 지정하도록 할 수 있지만, 이는 프로그램을 삽입 공격에 취약하게 만든다. 대신 조건문을 사용하는 것이 좋다.

다음은 사용자로부터 검색 키워드(제목, 내용, 번호)를 입력받아 조회하는 프로그램이다.

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
Scanner keyScan = new Scanner(System.in);

System.out.print("항목(1:번호, 2:제목, 3: 내용, 그 외: 전체)? ");
String item = keyScan.nextLine();

System.out.print("검색어? ");
String keyword = keyScan.nextLine();

keyScan.close();

// SQL 매퍼에 여러 개의 파라미터 값을 넘길 때 주로 Map을 사용한다.
HashMap<String, Object> params = new HashMap<>();
params.put("item", item);
params.put("keyword", keyword);

List<Board> list = sqlSession.selectList("BoardMapper.select4", //
                                         params);

for (Board board : list) {
  System.out.printf("%d, %s, %s, %s, %d\n", //
                    board.getNo(), //
                    board.getTitle(), //
                    board.getContent(), //
                    board.getRegisteredDate(), //
                    board.getViewCount());
}

sqlSession.close();

keyword가 포함된 데이터를 검색하기 위해서 %로 해당 키워드를 감싸줘야 한다. 이는 자바 코드에서 직접 할 수도 있고, sql의 concat() 연산자를 이용해 할 수도 있다. 이 방법이 더 편하긴 하다.

검색 문자열 내에서 %를 사용하면 개수에 상관없이 모든 문자를 의미한다. where 이름 like 'cake%'은 cake 단어로 시작하는 모든 데이터를 검색하고, %cake%를 주면 cake 단어를 포함하는 모든 데이터를 검색하고, %cake를 주면 cake이라는 단어로 끝나느 모든 데이터를 검색한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="select4" resultMap="BoardMap" parameterType="map">
  select 
    board_id, title, contents, created_date, view_count 
  from x_board
  <if test="item == 1">
    where board_id = #{keyword}
  </if>
  <if test="item == 2">
    where title like concat('%', #{keyword}, '%')
  </if>
  <if test="item == 3">
    where contents like concat('%', #{keyword}, '%')
  </if>
</select>

이때, if 태그에서 조건을 줄 때 test의 속성값은 mybatis 문법을 사용한다. 그래서 item==1이어도 괜찮은 것이다.

<where>: 여러 개의 조건 합쳐서 검색

다음과 같이 여러 조건을 or로 연결해야 할 때가 있다. 그러나 이 접근법에는 치명적인 문제가 있다. no!=null이어서 첫번째 <if> 태그 안의 sql문이 실행되지 않는다고 하자. 그러면 where or title like '%title%'과 같이 where 바로 뒤에 or가 오는 잘못된 SQL문을 생성한다.

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
HashMap<String, Object> params = new HashMap<>();

Scanner keyScan = new Scanner(System.in);

System.out.print("번호? ");
String value = keyScan.nextLine();
if (value.length() > 0) {
  params.put("no", value);
}

System.out.print("제목? ");
value = keyScan.nextLine();
if (value.length() > 0) {
  params.put("title", value);
}

System.out.print("내용? ");
value = keyScan.nextLine();
if (value.length() > 0) {
  params.put("content", value);
}

List<Board> list = sqlSession.selectList("BoardMapper.select5", params);

for (Board board : list) {
  System.out.printf("%d, %s, %s, %s, %d\n", 
                    board.getNo(), 
                    board.getTitle(), 
                    board.getContent(), 
                    board.getRegisteredDate(), 
                    board.getViewCount());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="select5" resultMap="BoardMap" parameterType="map">
  select 
    board_id, title, contents, created_date, view_count 
  from x_board
  <if test="no != null">
    board_id = #{no}
  </if>
  <if test="title != null">
    or title like concat('%', #{title}, '%')
  </if>
  <if test="content != null">
    or contents like concat('%', #{content}, '%')
  </if>
</select>
1
2
3
4
5
6
번호? 
제목? aaa
내용? 
Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: 
### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: (conn=348) You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'or title like concat('%', 'aaa', '%')' at line 11
### The error may exist in com/eomcs/mybatis/ex03/BoardMapper.xml

임의 조건 삽입

or 앞에 조건이 없을 경우를 대비하여 임의 조건(무조건 false)을 삽입하였다. 단 임의 조건은 실행에 영향을 끼치지 않아야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="select6" resultMap="BoardMap" parameterType="map">
  select 
    board_id, title, contents, created_date, view_count 
  from x_board
  where 1=0 
  <if test="no != null">
    or board_id = #{no}
  </if>
  <if test="title != null">
    or title like concat('%', #{title}, '%')
  </if>
  <if test="content != null">
    or contents like concat('%', #{content}, '%')
  </if>
</select>  

그러나 이 방법도 문제가 있다. 조건이 없으면 아예 선택이 안된다. 보통 아무 조건에도 걸리지 않는다면 전체 검색 결과가 나와야 하기 때문에 다른 방법이 필요하다.

image

위와 같이 여러 조건 중에서 빠지더라도 결과가 잘 나와야 한다.

where 사용 후

<where> 태그는

  • or/and 앞에 조건이 없을 대 or/and를 자동으로 제거한다.
  • where 조건이 없을 때는 where를 생성하지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<select id="select7" resultMap="BoardMap" parameterType="map">
  select 
    board_id, title, contents, created_date, view_count 
  from x_board
  where 1=0 
  <where> <!--  -->
    <if test="no != null">
      board_id = #{no}
    </if>
    <if test="title != null">
      or title like concat('%', #{title}, '%')
    </if>
    <if test="content != null">
      or contents like concat('%', #{content}, '%')
    </if>
  </where>
</select>  

<trim>

  • <where> 대신에 <trim>을 사용하여 불필요한 SQL 코드를 제거할 수 있다.
  • or/and 앞에 조건이 없을 때 or/and를 자동으로 제거한다.
  • <where>보다는 정교하게 사용할 수 있다.
  • <where>로 대체할 수 있다면 그냥 <where>를 사용하는 것이 낫다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  <select id="select8" resultMap="BoardMap" parameterType="map">
  select 
    board_id, title, contents, created_date, view_count 
  from x_board
    <trim prefix="where" prefixOverrides="OR | AND"> 
      <!-- or/and 앞에 아무것도 없을 때 or/and 를 자동으로 제거한다. -->
      <if test="no != null">
        board_id = #{no}
      </if>
      <if test="title != null">
        or title like concat('%', #{title}, '%')
      </if>
      <if test="content != null">
        or contents like concat('%', #{content}, '%')
      </if>
    </trim>
  </select>  

PMS 프로젝트에 Mybatis 기타 기능 활용하기

1단계: fully-qualified class name에 대해 별명 부여

src/main/com/resources/com/eomcs/pms/conf/mybatis-config.xml

클래스 이름에 대해 별명을 지정한다.

1
2
3
4
5
6
	<typeAliases>
		<typeAlias type="com.eomcs.pms.domain.Board" alias="board"></typeAlias>
		<typeAlias type="com.eomcs.pms.domain.Member" alias="member"></typeAlias>
		<typeAlias type="com.eomcs.pms.domain.Project" alias="project"></typeAlias>
		<typeAlias type="com.eomcs.pms.domain.Task" alias="task"></typeAlias>
	</typeAliases>

src/main/resources/com/eomcs/pms/mapper/XxxMapper.xml 도 찾아바꾸기 기능을 이용해 fully-qualified class name을 별명으로 대체한다.

일부 자바 클래스에 대해서 내장 별칭이 있다.

별칭매핑된 타입
_bytebyte
_longlong
_shortshort
_intint
_integerint
_doubledouble
_floatfloat
_booleanboolean
stringString
byteByte
longLong
shortShort
intInteger
integerInteger
doubleDouble
floatFloat
booleanBoolean
dateDate
decimalBigDecimal
bigdecimalBigDecimal
objectObject
mapMap
hashmapHashMap
listList
arraylistArrayList
collectionCollection
iteratorIterator

출처: https://mybatis.org/mybatis-3/ko/configuration.html#typeAliases

2단계: 특정 패키지에 소속된 전체 클래스에 대해 별명 부여하기

다음과 같이 <package> 태그를 활용하면 도메인 객체가 많을 때 손쉽게 별명을 부여할 수 있다.

1
2
3
4
5
6
7
8
9
<typeAliases>
  <package name="com.eomcs.pms.domain"/>
  <!-- 기존 각 클래스마다 별명 붙였던 방법
  <typeAlias type="com.eomcs.pms.domain.Board" alias="board"></typeAlias>
  <typeAlias type="com.eomcs.pms.domain.Member" alias="member"></typeAlias>
  <typeAlias type="com.eomcs.pms.domain.Project" alias="project"></typeAlias>
  <typeAlias type="com.eomcs.pms.domain.Task" alias="task"></typeAlias>
   -->
</typeAliases>

3단계: 게시글 검색 기능을 추가한다.

마이바티스 if 태그를 사용하여 동적 SQL을 작성한다.

  • 검색어에 해당하는 게시글이 없을 경우,
    1
    2
    
    명령> /board/search
    검색어? ㅋㅋ
    
  • 검색어를 입력하지 않을 경우
    1
    2
    3
    4
    5
    6
    7
    8
    
    명령> /board/search
    검색어?
    번호, 제목, 작성자, 등록일, 조회수
    13, okok4, ccc, 2020-11-09, 0
    12, okok2, aaa, 2020-11-09, 0
    10, test, ggg, 2020-11-06, 0
    9, hul..., aaa, 2020-11-05, 0
    8, okok, ggg, 2020-11-05, 0
    

mybatis의 if 태그를 사용하여 동적 SQL을 작성한다.

검색어에 해당하는 게시글이 있을 경우

BoardListCommand

1
List<Board> list = boardDao.findAll(null);

BoardSearchCommand

1
List<Board> list = boardDao.findAll("%" + keyword + "%");

BoardDao

1
List<Board> findAll(String keyword) throws Exception;

BoardDaoImpl

1
2
3
4
5
6
  @Override
  public List<Board> findAll(String keyword) throws Exception {
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("BoardDao.findAll", keyword);
    }
  }

BoardMapper.findAll

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  <select id="findAll" resultMap="BoardMap" parameterType="string">
    select 
      b.no, 
      b.title, 
      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
    <if test="keyword != null">
   	  where 
   	  	b.title like #{keyword}
   	  	or b.content like #{keyword}
   	  	or m.name like #{keyword}
    </if>
    order by 
      b.no desc
  </select>

4단계 - 프로젝트 검색 기능을 추가한다.

마이바티스의 if 태그를 여러 개 사용하여 동적 SQL을 작성한다.

1
2
3
4
5
6
명령> /project/search
항목(1:프로젝트명, 2:관리자명, 3:팀원, 그 외: 전체)? 1
검색어? java
번호, 프로젝트명, 시작일 ~ 종료일, 관리자, 팀원
21, javajavaxx, 2020-02-02 ~ 2020-03-03, aaa, [ccc,ddd]
17, java1, 2020-01-01 ~ 2020-02-02, aaa, []
  • com.eomcs.pms.dao.ProjectDao 인터페이스 변경
    • findByKeyword(String item, String keyword) 을 추가한다.
  • com.eomcs.pms.dao.mariadb.ProjectDaoImpl 클래스 변경
    • findByKeyword(String item, String keyword) 를 구현한다.
1
2
3
4
5
6
7
8
9
  public List<Project> findByKeyword(String item, String keyword) throws Exception {
    HashMap<String,Object> map = new HashMap<>();
    map.put("item", item);
    map.put("keyword", keyword);

    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
      return sqlSession.selectList("ProjectDao.findByKeyword", map);
    }
  }
  • src/main/resources/com/eomcs/pms/mapper/ProjectMapper.xml 변경
    • findByKeyword SQL 문을 추가한다.
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
<select id="findByKeyword" parameterType="map" resultMap="ProjectMap">
  select
		p.no,p.title,p.sdt,p.edt,
		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 
	<if test="item == 1">
  	  where 
  	  	p.title like concat('%', #{keyword}, '%')
   </if>
   <if test="item == 2">
  	  where 
  	  	m.name like concat('%', #{keyword}, '%')
   </if>
   <if test="item == 3">
  	  where 
  	  	m2.name concat('%', #{keyword}, '%')
   </if>
	order by p.no desc
</select>
  • com.eomcs.pms.handler.ProjectSearchCommand 클래스 생성
    • ProjectDao.findByKeyword() 을 사용하여 검색 기능을 처리한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
String item = Prompt.inputString(
  "항목(1:프로젝트명, 2:관리자명, 3:팀원, 그 외: 전체)? ");
String keyword = Prompt.inputString("검색어? ");

List<Project> list = projectDao.findByKeyword(item, keyword);
System.out.println("번호, 프로젝트명, 시작일 ~ 종료일, 관리자, 팀원");

for (Project project : list) {
  StringBuilder members = new StringBuilder();
  for (Member member : project.getMembers()) {
    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());
}

5단계: 프로젝트 상세 검색 기능을 추가한다.

마이바티스의 where 태그를 사용하여 동적 SQL을 작성한다.

1
2
3
4
5
6
7
8
명령> /project/detailSearch
[프로젝트 상세 검색]
프로젝트명? java
관리자명? aaa
팀원? bbb
번호, 프로젝트명, 시작일 ~ 종료일, 관리자, 팀원
21, javajavaxx, 2020-02-02 ~ 2020-03-03, aaa, [ccc,ddd]
17, java1, 2020-01-01 ~ 2020-02-02, aaa, []
  • src/main/resources/com/eomcs/pms/mapper/ProjectMapper.xml 변경
    • findByDetailKeyword SQL 문을 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<select id="findByDetailKeyword" resultMap="ProjectMap" parameterType="map">
    select
		p.no,p.title,p.sdt,p.edt,
		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 
  <where>
    <if test="title != null">
      p.title like concat('%', #{title}, '%')
    </if>
    <if test="owner != null">
      and m.name like concat('%', #{owner}, '%')
    </if>
    <if test="member != null">
      and m2.name like concat('%', #{member}, '%')
    </if>
  </where>
  order by p.no desc
</select>
  • com.eomcs.pms.dao.ProjectDao 인터페이스 변경
    • findByDetailKeyword(Map<String,Object> keywords) 을 추가한다.
  • com.eomcs.pms.dao.mariadb.ProjectDaoImpl 클래스 변경
    • findByDetailKeyword(String item, String keyword) 를 구현한다.
1
2
3
4
5
public List<Project> findByDetailKeyword(Map<String,Object> keywords) throws Exception {
  try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
    return sqlSession.selectList("ProjectDao.findByDetailKeyword", keywords);
  }
}
  • com.eomcs.pms.handler.ProjectDetailSearchCommand 클래스 생성
    • ProjectDao.findByDetailKeyword() 을 사용하여 검색 기능을 처리한다.
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
HashMap<String,Object> keywords = new HashMap<>();

String title = Prompt.inputString("프로젝트명? ");
if (title.length() > 0) {
  keywords.put("title", title);
}

String owner = Prompt.inputString("관리자명? ");
if (owner.length() > 0) {
  keywords.put("owner", owner);
}

String member = Prompt.inputString("팀원명? ");
if (member.length() > 0) {
  keywords.put("member", member);
}

List<Project> list = projectDao.findByDetailKeyword(keywords);
System.out.println("번호, 프로젝트명, 시작일 ~ 종료일, 관리자, 팀원");

for (Project project : list) {
  StringBuilder members = new StringBuilder();
  for (Member m : project.getMembers()) {
    if (members.length() > 0) {
      members.append(",");
    }
    members.append(m.getName());
  }

  System.out.printf("%d, %s, %s ~ %s, %s, [%s]\n",
                    project.getNo(),
                    project.getTitle(),
                    project.getStartDate(),
                    project.getEndDate(),
                    project.getOwner().getName(),
                    members.toString());
}
  • com.eomcs.pms.listener.AppInitListener 클래스 변경
    • /project/detailSearch 를 처리할 ProjectDetailSearchCommand 객체를 등록한다.
This post is licensed under CC BY 4.0 by the author.

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

HTML5 CSS3 웹 표준의 정석 #4장: 폼 관련 태그들

Loading comments from Disqus ...