JAVA

Mybatis:설정하기

docc 2021. 7. 27. 16:23

Mybatis

참고사이트

https://www.baeldung.com/mybatis

이제 DAO 와 같이 DB 연결하는 Mapper 를 사용해 더 쉽게 연결하자!
DAO는 JDBC API 였고, 이제 Mybatis API 를 설치한다. JDBC 를 위한 ConnectionPool API 도 필요 없다.
DAO - 인터페이스를 만들고 그걸 구현한 클래스에 @Repository 를 걸어 @Autowired 시 인터페이스형으로 선언하여 사용함
Mapper - 인터페이스에 아예 @Mapper 를 걸면 @Autowired 시 알아서 상속받은 클래스가 있는것 처럼 선언되어 사용할 수 있다.
(구현하는 클래스 필요가 없다.)
DAO vs Mapper : 여러개를 한번에 저장시 DAO 는 중간에 실패해도 초반건 들어가버린다. Mapper는 전체를 롤백한다.
(ex. board는 저장했는데 attach가 이상하다고 실패했을 때 전체를 실행 시키지 않는 롤백을 시켜야 좋다)

Mybatis 설정하기! (아주중요!!!)

>> pom.xml

		<!-- mybatis 를 위한 API 3개 -->
		<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
		<dependency>
		    <groupId>org.mybatis</groupId>
		    <artifactId>mybatis</artifactId>
		    <version>3.4.1</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring -->
		<dependency>
		    <groupId>org.mybatis</groupId>
		    <artifactId>mybatis-spring</artifactId>
		    <version>1.3.0</version>
		</dependency>
		<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc -->
		<dependency>
		    <groupId>org.springframework</groupId>
		    <artifactId>spring-jdbc</artifactId>
		    <version>5.0.2.RELEASE</version>
		</dependency>

		<!-- db connectionpool 중 아래 2개는 필요없다. -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-dbcp2</artifactId>
			<version>2.7.0</version>
		</dependency>
// 		<dependency>
//			<groupId>org.apache.commons</groupId>
//			<artifactId>commons-pool2</artifactId>
//			<version>2.8.0</version>
//		</dependency>
//		<dependency>
//			<groupId>commons-logging</groupId>
//			<artifactId>commons-logging</artifactId>
//			<version>1.2</version>
//		</dependency>

>> web.xml

	<servlet>
		<servlet-name>dispatcher</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextClass</param-name>
			<param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
		</init-param>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>
				config.MvcConfig
				config.BeanConfig
			</param-value>
		</init-param>
	</servlet>

web.xml 에 DispatcherServlet 이

설정된 init-param 의 @Configuration 클래스들을 읽어 빈등록 한 것들을 반영해준다!


>> MybatisConfig.java

package config;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan(basePackages = {"com.stone"})
public class MyBatisConfig {
	
	@Bean
	public DataSource dataSource() {
		String driverClassName = "com.mysql.cj.jdbc.Driver";
		String url = "jdbc:mysql://localhost:3306/mvc2?useUnicode=true";
		String userName = "root";
		String password = "1234";
		BasicDataSource dataSource = new BasicDataSource();
		dataSource.setDriverClassName(driverClassName);
		dataSource.setUrl(url);
		dataSource.setUsername(userName);
		dataSource.setPassword(password);
		
		return dataSource;
	}
	
	@Bean
	public SqlSessionFactory sqlSessionFactory() throws Exception {
		SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
		sqlSessionFactoryBean.setDataSource(dataSource());
		return sqlSessionFactoryBean.getObject();
	}
	
	@Bean
	public SqlSessionTemplate sqlSessionTemplate() throws Exception {
		return new SqlSessionTemplate(sqlSessionFactory());
	}
}
@MapperScan(basePackages = {"com.stone"}) : 패키지 아래에 mybatis 인 객체인 @Mapper 애들 스캔
@ComponentScan(basePackages = "com.stone") : 스프링객체인 @Controller 과 @Repository 인 애들 스캔, 자동으로 Bean등록
BeanConfig 에 @Configuration 을 달아 따로 @Bean 을 잡는 이유 ( 생성자에 셋팅을 해야 할 수 있으니까!)
@Bean 등록이 많은 이유: 아래의 생성자에 설정을 해야하며 설정시 쓰는 객체를 개인의 판단에 따라 여러가지 사용 가능하기 때문이다.
아래의 3개 Bean 등록은 필수다!!
1. DataSource : Connection 해준다. 그 중 BasicDataSource 객체를 선택했다.
대표적으로 DriverManagerDataSource() 와 BasicDataSource() 골라쓸 수 있다.
하지만 전자의 경우 계속 Connection 을 새로 생성하며 후자는 connection pooling 이라서 후자를 선택한다!
(10개정도 만들어놓고 필요할때 꺼내쓰는방식)/connection pool 을 이제 안써도 됨!
2. SqlSessionFactory : Connection 을 가져와 연결한 sqlFactory 를 만들어준다. 그 중 SqlSessionFactoryBean 객체를 선택했다.
3. SqlSessionTemplate : sql문(select,insert등) 을 사용하게 하는 SqlSessionTemplate 객체를 Bean으로 올려 생성한다.

>> MvcConfig.java (@Import 로 MyBatisConfig 추가)

@Import(value= {MyBatisConfig.class})
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.stone")
public class MvcConfig implements WebMvcConfigurer {

	@Override
	public void configureViewResolvers(ViewResolverRegistry registry) {
		registry.jsp("/WEB-INF/views/",".jsp");
	
	}
	
	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		// addResourceHandler = 모든 요청명 풀네임, addResourceLocations = 요청명 중 폴더명
		registry.addResourceHandler("/img/**").addResourceLocations("/img/");
		registry.addResourceHandler("/upload/**/**").addResourceLocations("/upload/");
		registry.addResourceHandler("/js/*.js").addResourceLocations("/js/");
	}
	
}

예시

@Select("select no,name from member where id=#{id} and password=#{password}")
HashMap<String, Object> isIn(@Param("id")String id, String password)

Member (회원)

>> IMemberMapper

package com.stone.mvc.dataservice_member;

import java.util.List;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import com.stone.mvc.common_member.Member;

@Mapper
public interface IMemberMapper {

	@Select("select * from member;")
	List<Member> selectAll();
	
	@Select("select * from member where no=#{no}")
	Member findByNo(@Param("no") int 회원번호);

	@Insert("insert into member(name,post,address,detailaddress,tel,"
	+"email,id,password,profile)"
	+"values(#{name},#{post},#{address},#{detailaddress},"
	+"#{tel},#{email},#{id},#{password},#{profile})")
	void save(Member member);
	
	@Select("select no,name from member where id=#{id} and password=#{password}")
//no=1 , name=나길동 이렇게 HashMap 으로 컬럼명과 컬럼값이 들어간다.
//만약 하나의 튜플이 아닌 여러개의 튜플이 나온다면 List<HashMap<String,String>> 으로 해주면 된다.
	HashMap<String, String> isIn(String id, String password);
	
	@Select("select if(count(*)=1,1,0) from member where id=#{id}")
	boolean isIn2(String id);//숫자가0 은 false로 반환, 0외의 모든 수는 true 임을 기억! DB에는 true false 가 없음
}
@Mapper : JDBC 에서는 @Repository 였던 것처럼 DB연동하는 클래스라고 선언
(이제 MyBatisConfig 로 설정한 환경의 DB연결 설정이 알아서 셋팅된다.)
select 문으로 db에 접근을 선언. 아래 매서드에 알아서 열이름=멤버변수이름을 매핑해서 넣어준다.
@Param("#{no}이름과같은이름") : sql 문의 값에 매칭되는 매개변수 선언(변수명이 같다면 생략가능)

>> MemberDAO ( DAO 의 일을 Mapper 로 돌리자 + Mapper 쪽에서 HashMap으로 바꿨으니 여기서도 반영하기 )

@Repository
public class MemberDAO implements IMemberDAO {
	@Autowired
	IMemberMapper memberMapper;
	
	// 회원 저장하기
	public void save(Member member) {
		memberMapper.save(member);
	}

	// 존재하는 회원인지(로그인시 사용) 있으면 번호와 성명반환
	public HashMap<String,Object> isin(String id, String password) {
		return memberMapper.isIn(id, password);
	}

	// 존재하는 아이디인지(회원가입시 아이디중복체크로 사용)
	public boolean isin(String id) {
		return memberMapper.isIn2(id);
	}
}

>> 회원등록컨트롤 (Mapper 쪽에서 HashMap으로 바꿨으니 여기서도 반영하기)

HashMap<String, Object> NoAndName = memberDAO.isin(member.getId(), member.getPassword());
session.setAttribute("no", NoAndName.get("no"));
session.setAttribute("name", NoAndName.get("name"));

Board (게시물)

>> IAttachMapper.java

package com.stone.mvc.dataservice_board;

import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;

import com.stone.mvc.common_board.Attach;

@Mapper
public interface IAttachMapper {
	
	@Insert("insert into attach(name,size,board_no) values(#{name},#{size},#{board.no})")
	void save(Attach 첨부);
	
	@Insert({
		"<script>",
		"insert into attach(name,size,board_no) values",
		"<foreach collection='attachs' item='attach' index='index' separator=','>",
			"(#{attach.name},#{attach.size},#{attach.board.no})",
		"</foreach>",
		"</script>"
	})
	void saveAttachs(@Param(value="attachs")List<Attach> attachs);
	
	@Select("select * from attach where board_no=#{board_no}")
	List<Attach> selectByBoardNo(@Param("board_no") int 게시물번호);

	@Select("select * from attach where no=#{no}")
	@Results(value = {
			@Result(property="no", column="no"),
			@Result(property="name", column="name"),
			@Result(property="size", column="size"),
			@Result(property="board.no", column="board_no")})
	Attach findByNo(@Param("no") int 첨부번호);
}
1. board.no : Attach 의 board 참조변수의 변수인 no 을 가져온다.
2. 반복insert문 사용하기 : {} 로 감싸야함. 각 문장의 구분자는 '+' 가 아닌 ',' 이다.
<script> : 반복되는 문장들이 바로바로 insert 되지 않고 한번에 모았다 가도록 잡아놓는다.
<foreach> : 향상된 for문
- collection : 매개변수에서 가져오는 배열형태
- item : collection 에서 가져온 배열을 하나씩 빼서 넣기
- index : 반복수
- separator : 반복되는 문장 끝에오는 구분자
3. <ResultMap>(xml) = @Results(config)
@Results
- value : 여러 @Result 설정
@Result : property 와 column 을 매핑시켜줌
- property : 객체의 멤버변수명
- column : 테이블 컬럼명 //as 로 별칭만들 경우 그 별칭!(어디까지나 추출된 테이블의 컬럼명이다.)

>> IBoardMapper

@Mapper
public interface IBoardMapper {
	@Insert("insert into board(title,contents,writer) values(#{title},#{contents},#{writer.no})")
	@SelectKey(statement="select last_insert_id()", keyProperty="no", before=false, resultType=int.class)
	int saveOnlyBoard(Board board);//attach 가 저장이 안됨. insert 할 때의 one to many 관계

	@Select("select * from board where no=#{no}")
	@Results(value= {
			@Result(property="no",column="no"),
			@Result(property="title",column="title"),
			@Result(property="contents",column="contents"),
			@Result(property="wdate",column="wdate"),
			@Result(property="writer",column="writer",one=@One(select="com.stone.mvc.dataservice_member.IMemberMapper.findByNo")),
			@Result(property="views",column="views"),
			@Result(property="attachs",javaType=List.class,column="no",many=@Many(select="com.stone.mvc.dataservice_board.IAttachMapper.selectByBoardNo"))
	})
	Board findByNo(int no);
	
	@Update("update board set views=views+1 where no=#{no}")
	void updateView(int no);
	
	@Select("select * from board")
	@Results(value= {
			@Result(property="no",column="no"),
			@Result(property="title",column="title"),
			@Result(property="contents",column="contents"),
			@Result(property="wdate",column="wdate"),
			@Result(property="writer",column="writer",one=@One(select="com.stone.mvc.dataservice_member.IMemberMapper.findByNo")),
			@Result(property="views",column="views"),
			@Result(property="attachs",javaType=List.class,column="no",many=@Many(select="com.stone.mvc.dataservice_board.IAttachMapper.selectByBoardNo"))
			})
	List<Board> selectAll();
1. saveOnlyBoard()
@SelectKey : 방금 insert 한 튜플의 key 값 가져오기
- statement : key 값 가져오는 sql 문
- keyProperty : key 값 컬럼이름, return 도 되고 board에 자동으로 이 이름의 멤버변수에 get된다.
( DAO 에서 따로 int no 로 받아 getNo(no) 하지 않아도 된다. )
- resultType : 나오는 key 값의 변수형
2. findByNo()
1) 게시물상세보기도 조회수update 문도 함께 날려야하지만 sql문은 여러번 날릴 수 없으므로 따로 만들어야 한다.
(DAO 에서 두 sql 문을 실행하면 된다. )

2) one to one 관계 : 게시물상세보기에 게시물 작성자 즉, Member 도 와야한다
@One : 자신(one도 되고 many도 될수 있음) to one 관계
column 값을 property에 바로 매핑하지 않고,
패키지명.클래스명.메서드명의 sql문에 column 값을 where에 넣어 결과값을 property에 매핑

3) @Many : 자신(one도 되고 many도 될수 있음) to many 관계
위와 동일
javaType : 여러개니까 List<Attach> 인 멤버변수에 List형으로 넣어준다고 표시
3. selectAll()
자신이 단일 객체던 List 형이던 위의 findByNo() 와 똑같이 매핑시켜 하나씩 List에 넣어준다.

>> BoardDAO ( DAO 의 일을 Mapper 로 돌리자 )

@Repository
public class BoardDAO implements IBoardDAO {
	
	@Autowired
	private IBoardMapper boardMapper;
	
	@Autowired
	private IAttachMapper attachMapper;
	
	//게시물 저장하기.
	public int save(Board board) {
		boardMapper.saveOnlyBoard(board);
		attachMapper.saveAttachs(board.getAttachs());//for문 안쓰고 sql문으로 한번에 저장이 가능해졌다(script-foreach문)
		
		return board.getNo();
	}
		
	// 게시물의 번호로 게시물의 상세내역 보기(+ 조회수 증가여부)
	public Board findByNo(int no, boolean addView) {
		if(addView) {boardMapper.updateView(no);}
		return boardMapper.findByNo(no);
	}


	// 게시물전체 가져오기
	public List<Board> selectAll() {
		return boardMapper.selectAll();
	}
}

>> ICommentMapper

@Mapper
public interface ICommentMapper {
	@Insert("insert into comment(contents,writer,board_no) values(#{contents},#{writer.no},#{board.no}")
	void save(Comment comment);
	
	@Select("select * from comment where board_no=#{no} order by wdate desc limit 0,#{size}")
	@Results(value= {
			@Result(property="no",column="no"),
			@Result(property="contents",column="contents"),
			@Result(property="wdate",column="wdate"),
			@Result(property="writer.no",column="writer"),
			@Result(property="board.no",column="board_no")
	})
	List<Comment> selectByBoardNo(int no, int size);
	
	@Select("select count(*) from comment where board_no=#{no}")
	int totalSizeByBoardNo(int no);
}
1. board 에 달린 모든 Comment 객체와 댓글총수 구하는 메서드도 분리해야 한다.(sql문이 2개 였으니까)
//굳이 board 객체를 다 채울 필요 없으니 no 만 셋팅했다.
(만약 board 객체 다채울라면 one to one(댓글기준관계) 로 board의 메서드 끌어와야한다.)

>> CommentDAO ( DAO 의 일을 Mapper 로 돌리자 )

@Repository
public class CommentDAO implements ICommentDAO {
	@Autowired ICommentMapper commentMapper;

	@Override
	public void save(Comment comment) {
		commentMapper.save(comment);
		
	}

	@Override
	public List<Comment> selectByBoardNo(int no, int size, RefInteger totalSize) {
		totalSize.value = commentMapper.totalSizeByBoardNo(no);
		return commentMapper.selectByBoardNo(no, size);
	}
}