[JDBC] JdbcTemplate 사용하고 적용해보기

ziwww·2024년 2월 26일

개발

목록 보기
4/14

JdbcTemplate란?

jdbcTemplate는 SQL를 직접 사용하여 JDBC를 다루는 편리한 방법이다.

  • spring-jdbc라이브러리에 JdbcTEmplate가 속해있기 때문에 별도의 복잡한 설정 없이 사용할 수 있다.
  • 템플릿 콜뱃 패턴을 사용해서 JDBC를 직접 사용할 때 발생하는 대부분의 반복 작업을 대신 처리해 준다.
  • 개발자는 SQL을 작성하고 전달할 파라미터를 정의하여 응답 값을 매핑하면 된다.
  • 단점: 동적 SQL을 작성하기 어렵다
    • 복잡하지 않은 SQL을 처리할때 사용하면 좋다.

JdbcTemplate 사용 설정

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

기본 사용법

template.update

  • 데이터를 변경할 때 사용
  • INSERT, UPDATE, DELETE SQL에 사용한다,

template.queryForObject()

  • 데이터를 하나 조회할 때 사용한다
  • 결과가 없으면 EmptyResultDataAccessException 예외가 발생한다.
  • 결과가 둘 이상이면 IncorrectResultSizeDataAccessException 예외가 발생한다.

template.query()

  • 데이터를 하나 이상 조회할 때 사용, list로 반환한다.
  • 결과가 없을 시 빈 컬렉션을 반환한다.

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

파라미터 '?' 바인딩

  • DB에 보낼 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 = ?";
        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에 담을 것이다.

quiz 게시물에 포함 되어있는 tag들 불러오기

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

데이터 베이스에서 자동으로 생성된 키 값을 가져오는 방법
만약 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

profile
반갑습니다. 오늘도 즐거운 하루입니다.

0개의 댓글