Spring

SpringBoot로 h2 DB와 연동해서 데이터 테이블 저장하기

지안22 2023. 4. 8. 01:01

1. 환경설정 하기

build.gradle 파일의 dependencies에 해당 내용을 추가한다. (Jdbc, h2)

// jdbc
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// h2 db
// implementation 'com.h2database:h2' // 2-1
runtimeOnly 'com.h2database:h2' // 2-2

Jdbc

1. Jdbc 사용을 위한 Spring Boot 의존성 추가. DataSource 구현체로 tomcat-jdbc을 제공해준다. JdbcTemplate 사용 가능.

 

h2 DB

2-1. SpringBoot가 h2 데이터 베이스를 기본 데이터 베이스로 사용한다는 의미. application.

2-2. 런타임 시점에만 의존하게 된다.

 

application.properties 파일에 해당 내용을 추가한다. (h2)

# DB
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:~/spring-qna-db
spring.datasource.username=sa
spring.datasource.password=

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

1. 드라이버는 h2를 쓰겠다.

2. 사용할 db 파일 경로 (= 최상위 디렉토리에 있는 spring-qna-db.mv.db 파일)

3. 디폴트 이름 

4. 디폴트 비밀번호 (없음)

5. h2 console을 활성화 한다.

6. h2 console의 url 경로 (예시 = http://localhost:8080/h2-console)

 

2. h2 console에서 테이블 만들기

h2 console

connect 버튼을 누른다.

- 첫 실행이라면 최상위 디렉토리에 spring-qna-db.mv.db 파일이 자동으로 만들어지게 된다.

 

 

 🤯 오류가 났을 경우 (해당 경로에 파일이 없다, 잘못된 접근 등등..)

 

1. 해당 파일을 삭제한다. (.mv.db 확장자, 같은 이름의 trace 확장자도 있으면 삭제.)

rm spring-qna-db.mv.db

2. 똑같은 이름으로 직접 파일을 생성한다.

touch spring-qna-db.mv.db

 

sql 쿼리 문으로 테이블 만들기

테이블 생성하는 쿼리 문

1. id: long형 숫자, null 불가, 자동으로 카운트 되면서 숫자가 올라감

2. writer: 16자리 까지의 문자가 가능하며 가변적인 공간을 가짐, null 불가

3. title: 32자리 까지의 문자가 가능하며 가변적인 공간을 가짐, null 불가

4. contents: 255자리 까지의 문자가 가능하며 가변적인 공간을 가짐, null 불가

5. createdAt:  날짜와 시간을 가짐, null 불가

6. points: long형 숫자, null 불가

7. primary key(id): 테이블 당 하나의 필드에 기본 키를 설정한다. 이 쿼리에서 기본 키는 id가 된다.

 

CHAR vs VARCHAR :

  • CHAR: 불변한 크기의 공간을 가짐. char(8) 일 때, 안에 설정한 값이 2글자라면 8개의 크기를 맞추기 위해서 6개의 공백이 자동으로 생성된다.
  • VARCHAR: 가변적인 공간을 가짐. varchar(8) 일 때, 안에 설정한 값이 2글자라면 2글자 만큼의 공간만 생성됨.

 

DATE vs TIME vs DATETIME vs TIMESTAMP

  • DATE: 날짜만 표시 (YYYY-MM-DD)
  • TIME: 시간만 표시 (HH:MM:SS)
  • DATETIME: 등록한 날짜와 시간을 표시 - 문자, 8byte (YYYY-MM-DD HH:MM:SS), 직접 입력해줘야만 날짜가 설정된다.
  • TIMESTAMP: 등록한 날짜와 시간을 표시 - 숫자, 4byte (YYYY-MM-DD HH:MM:SS), timezone 기반으로 자동으로 설정된다. timezone이 변경 될 경우, 자동으로 업데이트 된다.

 

3. JdbcTemplate을 사용해서 h2 DB와 연결하기

게시글을 DB에 저장하기 위한 JdbcArticleRepository 클래스를 만든다.

@Repository
public class JdbcArticleRepository implements ArticleRepository{

    private final JdbcTemplate jdbcTemplate;
    
    public JdbcArticleRepository(DataSource dataSource){
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

JdbcArticleRepository는 ArticleRepository의 구현체다.

필드로 JdbcTemplate을 가지고, 생성자의 파라미터로 DataSource를 주입받는다. (SpringBoot의 힘!)

 

Article 객체를 h2 DB에 저장한다.

@Override
public Article save(Article article) {
    // insert 쿼리???
    SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
    jdbcInsert.withTableName("articles_squad").usingGeneratedKeyColumns("id");

    Map<String, Object> parameters = new ConcurrentHashMap<>();
    parameters.put("writer", article.getWriter());
    parameters.put("title", article.getTitle());
    parameters.put("contents", article.getContents());
    parameters.put("createdAt", article.getCreatedAt());
    parameters.put("points", article.getPoints());

    Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
    article.setId(key.longValue());
    return article;
}

insert를 쉽게 해주기 위해 SimpleJdbcInsert를 생성한다.

"articles_squad"라는 이름을 가진 테이블에 "id"를 기본키로 가진 애를 생성해 줄 것이다.

 

Map의 value는 반환타입이 여러개일 수 있으니 최상위 타입인 Object로 설정해주고, 동시성 이슈를 방지하기 위해 ConcurrentHashMap을 사용한다.

 

Number 클래스: Character, Boolean을 제외한 모든 Wrapper 클래스의 상위 클래스.

db에서 자동으로 생성(AUTO_INCREMENT)되는 primary key를 받아서 article의 id에 넣어준다.

 

 

🤔 insert를 jdbcTemplate.update(sql)로 하면 더 간단할 것 같은데 왜 이렇게 쓰는 걸까?

update() 메서드는 변경 된 행의 개수만 리턴한다.

update() 메서드로는 AUTO_INCREMENT로 설정된 Primary Key를 알아낼 수 없기 때문에 위처럼 SimpleJdbcInsert 클래스를 사용해서 테이블 요소의 insert를 진행해준다.

 

 

  • 테이블 요소를 insert 해주는 방법은 SimpleJdbcInsert 클래스를 사용하는 방법 외에도 BeanPropertySqlParameterSource 클래스를 사용하는 방법 등이 있기 때문에 자신에게 맞는 방법을 참고 할 것!

 

Id(번호)로 게시글 찾기

@Override
public Optional<Article> findById(Long id) {
    List<Article> result = jdbcTemplate.query("select * from articles_squad where id = ?", articleRowMapper(), id);
    return result.stream().findAny();
}

 articles_squad 테이블에서 (파라미터로 받은 id와) 동일한 id를 가진 Article을 찾아, Optional로 감싸서 반환해준다.


findAny()와 findFirst()의 차이점

  • findAny(): 병렬처리에 용이하다. 제일 먼저 찾아지는 요소를 반환하기 때문에 결과값이 다를 수 있다.
  • findFirst(): 찾은 요소들 중, 가장 앞의 요소를 반환한다. 결과값은 동일하다.

 

table에 있는 모든 요소(Article) 반환하기

@Override
public List<Article> findAll() {
    return jdbcTemplate.query("select * from articles_squad", articleRowMapper());
}

articles_squad 테이블에 있는 모든 요소(Article)를 반환한다.

 

 

table에 있는 모든 요소(Article) 삭제하기

@Override
public void clearStore() {
    jdbcTemplate.update("delete from articles_squad");
}

article_squad 테이블에 있는 모든 요소(Article)를 삭제한다.

 

 

RowMapper 클래스 활용해서 객체(Article) 조회하기

private RowMapper<Article> articleRowMapper(){
    return (rs, rowNum) -> {
        Article article = new Article();
        article.setId(rs.getLong("id"));
        article.setWriter(rs.getString("writer"));
        article.setTitle(rs.getString("title"));
        article.setContents(rs.getString("contents"));
        article.setCreatedAt(rs.getTimestamp("createdAt").toLocalDateTime());
        article.setPoints(rs.getLong("points"));
        return article;
    };
}


RowMapper를 활용해서 객체를 조회할 수 있다.
RowMapper는 데이터베이스의 반환 결과인 ResultSet을 객체로 변환해주는 클래스다.
원래는 행의 번호를 가져와서 직접 입력해 줘야 했는데, 각 columnLabel(= "id", "writer" 등..)에 해당되는 값을 찾아와서 넣어준다.
rowNum은 반복되는 루프 중, 현재 행의 번호를 나타낸다. 람다식으로 간결하게 표현 할 수 있다.

 

  • RowMapper를 사용하는 방법 외에도 NamedParameterJdbcTemplate을 사용하는 방법도 있다!

 

전체 코드

package kr.codesqaud.cafe.repository;

import kr.codesqaud.cafe.domain.Article;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;

import javax.sql.DataSource;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

@Repository
public class JdbcArticleRepository implements ArticleRepository{

    private final JdbcTemplate jdbcTemplate;
    
    public JdbcArticleRepository(DataSource dataSource){
        this.jdbcTemplate = new JdbcTemplate(dataSource);
    }

    @Override
    public Article save(Article article) {
        SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
        jdbcInsert.withTableName("articles_squad").usingGeneratedKeyColumns("id");

        Map<String, Object> parameters = new ConcurrentHashMap<>();
        parameters.put("writer", article.getWriter());
        parameters.put("title", article.getTitle());
        parameters.put("contents", article.getContents());
        parameters.put("createdAt", article.getCreatedAt());
        parameters.put("points", article.getPoints());

        Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
        article.setId(key.longValue());
        return article;
    }

    @Override
    public Optional<Article> findById(Long id) {
        List<Article> result = jdbcTemplate.query("select * from articles_squad where id = ?", articleRowMapper(), id);
        return result.stream().findAny();
    }

    @Override
    public List<Article> findAll() {
        return jdbcTemplate.query("select * from articles_squad", articleRowMapper());
    }

    @Override
    public void clearStore() {
        jdbcTemplate.update("delete from articles_squad");
    }

    private RowMapper<Article> articleRowMapper(){
        return (rs, rowNum) -> {
            Article article = new Article();
            article.setId(rs.getLong("id"));
            article.setWriter(rs.getString("writer"));
            article.setTitle(rs.getString("title"));
            article.setContents(rs.getString("contents"));
            article.setCreatedAt(rs.getTimestamp("createdAt").toLocalDateTime());
            article.setPoints(rs.getLong("points"));
            return article;
        };
    }
}

 

 

 

참고 블로그:

https://dahye-jeong.gitbook.io/spring/spring/2021-02-15-spring-boot/2021-02-16-boot-h2

 

H2 DB 설정 - spring

H2 DB는 컴퓨터에 내장된 램(RAM) 메모리에 의존하는 자바 기반의 RDBMS이다. 용량이 적고, 브라우저 기반의 콘솔 등을 지원해 장점이 많다. 또한, SpringBoot에서 별도 DB를 설치하지 않고 바로 사용할

dahye-jeong.gitbook.io

https://codechacha.com/ko/java8-stream-difference-findany-findfirst/

 

Java - Stream findAny()와 findFirst()의 차이점

Stream에서 어떤 조건에 일치하는 요소(element) 1개를 찾을 때, findAny()와 findFirst() API를 사용할 수 있습니다. findAny()는 Stream에서 가장 먼저 탐색되는 요소를 리턴하고, findFirst()는 조건에 일치하는

codechacha.com

https://hyeon9mak.github.io/easy-insert-with-simplejdbcinsert/

 

SimpleJdbcInsert를 통한 쉬운 Insert

```java private final JdbcTemplate jdbcTemplate;

hyeon9mak.github.io

https://code-lab1.tistory.com/277

 

[Spring] JdbcTemplate이란? JdbcTemplate 사용법, RowMapper란?

JdbcTemplate이란? JdbcTemplate은 JDBC 코어 패키지의 중앙 클래스로 JDBC의 사용을 단순화하고 일반적인 오류를 방지하는데 도움이 된다. 개발자가 JDBC를 직접 사용할 때 발생하는 다음과 같은 반복 작

code-lab1.tistory.com