jdbcTemplate는 SQL를 직접 사용하여 JDBC를 다루는 편리한 방법이다.
bulid.gradle에 jdbcTemplate를 포함한 spring-jdbc 라이브러리를 추가한다.
maria db를 사용할 것이므로 maria db설정도 추가하였다.
implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
application.properties에 데이터베이스를 연결한다.
spring.datasource.driver-class-name=연결할 DB 드라이버
spring.datasource.url=jdbc:mysql://localhost:3306/연결할 DB명
spring.datasource.username=DB접속할ID
spring.datasource.password=DB접속할ID의 비밀번호
JdbcdTemplate는 dataSource가 필요하다.
JdbcTemplateQuizRepository의 생성자에서 dataSource를 주입받고, 생성자 내부에서 JdbcTemplate를 생성하는 방법을 이용한다.
@Repository
public class JdbcTemplateQuizRepository implements QuizRepository {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateQuizRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
...
default RowMapper<QuizDto> quizDtoRowMapper() {
return ((rs, rowNum) -> {
QuizDto quizDto=new QuizDto();
quizDto.setId(rs.getLong("post_id"));
quizDto.setUserId(rs.getLong("user_id"));
quizDto.setAuthor(rs.getString("author"));
quizDto.setTitle(rs.getString("title"));
quizDto.setContents(rs.getString("contents"));
quizDto.setCreatedAt(rs.getTimestamp("created_at"));
quizDto.setLike(rs.getInt("like"));
quizDto.setAnswer(rs.getString("answer"));
return quizDto;
});
}
String sql = "select p.id as post_id, p.user_id, p.title, p.contents, p.created_at, p.like, a.answer, " +
"u.user_id as author " +
"from post p " +
"inner join answer a on p.id = a.post_id " +
"inner join user u on p.user_id = u.id " +
"where p.id = ?";
QuizDto quizDto = jdbcTemplate.queryForObject(sql, quizDtoRowMapper(), id);
가져오고 싶은 데이터들은 이와 같다.
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class QuizDto {
private long id;
private long userId;
private String author;
private String title;
private String contents;
private String answer;
private java.sql.Timestamp createdAt;
private long like;
private List<TagDetailDto> tags;
}
id : quiz의 고유 번호
userId : user의 고유 번호
author : user의 닉네임(작성자)
title: quiz 제목
contents: quiz 문제
answer: quiz 정답
createdAt: quiz 생성 시간
like: 좋아요 갯수
tags: tag의 name과 고유 번호가 담겨있는 리스트
sql
String sql = "select p.id as post_id, p.user_id, p.title, p.contents, p.created_at, p.like, a.answer, " +
"u.user_id as author " +
"from post p " +
"inner join answer a on p.id = a.post_id " +
"inner join user u on p.user_id = u.id " +
"where p.id = ?";
- user의 닉네임(user_id)이 필요하므로 user table을 join하였다.
- 닉네임은 결국 작성자이므로 알아보기 쉽게 as author로 설정하였다.
- quiz의 id와 연결되어 있는 answer테이블의 answer값이 필요하므로 answer테이블을 join하였다.
rowMapper
default RowMapper<QuizDto> quizDtoRowMapper() {
return ((rs, rowNum) -> {
QuizDto quizDto=new QuizDto();
quizDto.setId(rs.getLong("post_id"));
quizDto.setUserId(rs.getLong("user_id"));
quizDto.setAuthor(rs.getString("author"));
quizDto.setTitle(rs.getString("title"));
quizDto.setContents(rs.getString("contents"));
quizDto.setCreatedAt(rs.getTimestamp("created_at"));
quizDto.setLike(rs.getInt("like"));
quizDto.setAnswer(rs.getString("answer"));
return quizDto;
});
}
- rowMapper를 이용해서 QuizDto에 데이터를 설정해준다.
- 아직 tag는 불러오지 못했기 때문에 setTags설정은 빼준다.
적용하기
QuizDto quizDto = jdbcTemplate.queryForObject(sql, quizDtoRowMapper(), id);
- 값 한개만 불러오기 때문에 queryForObject를 사용한다.
- 불러온 값을 객체로 QuizDto로 변환해야되기 때문에 아까 만들어줬던 quizDtoRowMapper()를 사용한다.
- 파라미터 '?'를 사용하였기 때문에 ?에 해당하는 값을 넘겨준다.
이렇게 하면 QuizDto에 tags를 제외한 모든 데이터가 들어온 상태가 된다.
이제 tag를 불러와서 QuizDto에 담을 것이다.
sql
String sql = "select t.id, t.name "+
"from tag t inner join quiztag qt on t.id = qt.tag_id "+
"where qt.post_id = ?";
- quiztag가 quiz테이블과 tag테이블을 이어주는 테이블이기 때문에 tag테이블과 quiztag를 join하였다.
- quiztag에서 quiz id에 해당하는 tag들만 불러온다
RowMapper()
default RowMapper<TagDetailDto> TagRowMapper() {
return (rs,rowNum)->{
TagDetailDto tagDetailDto = new TagDetailDto();
tagDetailDto.setId(rs.getLong("id"));
tagDetailDto.setName(rs.getString("name"));
return tagDetailDto;
};
}
- 데이터 조회 결과를 TagDetailDto 객체로 변환해야되기 때문에 TagDetailDto를 사용한다.
적용하기
jdbcTemplate.query(sql, TagRowMapper(), id);
- 값을 여러개 불러오기 때문에 query를 사용한다.
- 불러온 값을 객체로 TagDetailDto로 변환해야되기 때문에 아까 만들어줬던 TagRowMapper()를 사용한다.
- 파라미터 '?'를 사용하였기 때문에 ?에 해당하는 값을 넘겨준다.
quizDto에 tags데이터 넣어주기
quizDto.setTags(tags);
jdbcTemplate.query로 만들어준
List<TagDetailDto> tags의 데이터를 quizDto에 넣어준다.
이렇게 하면 원하는 데이터를 모두 불러 올 수 있다!
간단하게 로직 설명
1. 퀴즈 테이블에 새로운 데이터를 추가한다.(저장)
2. 저장하고 나서 자동으로 생성된 post_id를 keyholder를 이용해서 가져온다.
3. post_id를 이용해서 answer와 tag들도 데이터를 추가한다.
데이터 베이스에서 자동으로 생성된 키 값을 가져오는 방법
만약 keyHolder를 사용하지 않으면 update()메소드는 변경된 행의 개수만 리턴하므로 자동 생성된 키 값을 알아낼 수 없다.
퀴즈 테이블에 새로운 데이터 추가하기
//게시글 저장
String postSql = "insert into post (user_id, type, title, contents, created_at, updated_at, `like`) " +
"values (?, ?, ?, ?, ?, ?, ?)";
//생성된 키 값 받아오기
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(postSql, new String[]{"id"}); // "id"는 자동 생성된 키의 컬럼명
ps.setLong(1, userId);
ps.setString(2, "QUIZ");
ps.setString(3, quiz.getTitle());
ps.setString(4, quiz.getQuizContent());
ps.setObject(5, LocalDateTime.now());
ps.setObject(6, LocalDateTime.now());
ps.setInt(7, 0);
return ps;
}, keyHolder);
- keyHolder를 사용해야하기 때문에 new GeneratedKeyHolder()로 인스턴스 생성
- 파라미터로 전달받은 Connetcion으로
PreparedStatement를 생성한다.- 두번째 파라미터에 자동 생성되는 컬럼의 이름을 넣는다.
- PreparedStatement.setString(index, String)과 같이 자료형에 맞춰 메서드를 사용한다.
- jdbcTEmplate.update()의 두번째 파라미터로 keyHolder를 전달한다.
- preparedStatement를 반환해준다.
키 값 가져오기
Long postId = keyHolder.getKey().longValue();
키 값을 가져온다.
나머지 저장하기
String answerSql = "insert into answer(post_id, answer) " +
"values (?,?)";
String answer = quiz.getAnswerContent();
jdbcTemplate.update(answerSql, postId, answer);
String tagSql = "insert into quiztag (post_id, tag_id) values (?,?)";
String tags = quiz.getTags();
if (!tags.isEmpty()) {
String[] tagStrings = quiz.getTags().split(",");
for (String tag : tagStrings) {
jdbcTemplate.update(tagSql, postId, Integer.parseInt(tag));
}
}
만약 tag가 아무것도 없을 경우일 때("") split을 하면 빈 값을 저장해서 에러가 나기 때문에 조건식을 넣어주었다.
transaction하기
만약 save과정에서 하나가 실패했을 경우 그 전 데이터들은 들어있는 상태가 되기 때문에 에러가 발생할 수 있는 가능성이 있다.
save 과정에서 하나가 실패했을 경우 따라서 처음 상태로 되돌리는 transaction 어노테이션을 붙혀주었다.
//데이터 저장 도중 에러가 생길 경우 원 상태로 복귀
@Transactional
@Override
public Long saveQuiz(Quiz quiz, Long userId) {
return quizRepository.saveQuiz(quiz, userId);
}
참고한 블로그: https://innovation123.tistory.com/69