\h2\bin 폴더에 들어가서 h2.bat 실행 (웹 콘솔로 접근하는 법)
jdbc:h2:~/test : ~/test - 파일경로
나가기 버튼
C:\Users\사용자 디렉토리에 test.mv.db 파일 여부 확인
localhost로 접속 후 Jdbc URl 바꾸기 jdbc:h2:tcp://localhost/~/test
=> 파일을 직접 접근하는게 아니라 소켓을 통해서 접근이 가능하여 여러군데에서 접근 가능
문제가 있을 시 삭제 방법
연결 해제 후 rm test.mv.db -> 다시 접속(h2.bat 부터..)
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
조회
생성
insert into member(name) values('spring')
build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
resources/application.properties
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.url = < jdbc URL >
src/main/java/hello/hellospring/repository/JdbcMemberRepository.java
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
// 쿼리 짜기
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null; //결과 받기
//exception이 많기 때문에 try-catch를 잘 짜줘야한다!
try {
conn = getConnection(); //커넥션 가져오기
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); //sql : 21라인 쿼리문
pstmt.setString(1, member.getName()); //parameterIndex : 1 -> 21라인 첫번째 ? 와 매칭
pstmt.executeUpdate(); //실제로 Db에 쿼리가 날아가는 시점
rs = pstmt.getGeneratedKeys(); //getGeneratedKeys : RETURN_GENERATED_KEYS와 매칭되어서 사용
//값 꺼내고 세팅하기..
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e); //릴리즈 필수!!!!!
} finally {
close(conn, pstmt, rs);
}
}
//조회
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from member where id = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery(); //executeQuery: 조회
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member); //반환
} else {
return Optional.empty(); //없으면 Optional.empty()
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
//전체조회
@Override
public List<Member> findAll() {
String sql = "select * from member";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>(); //List 형태 주의
while(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
//조회 - 이름
@Override
public Optional<Member> findByName(String name) {
String sql = "select * from member where name = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
}
return Optional.empty();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
//Spring 프레임워크 사용 시 커넥션 가져오는 법
//직접 datasource.getConnection() 해도 되지만 계속 새로운 커넥션이 주어지기 때문에 DataSourceUtils 사용 -> 이전의 트랜잭션에 걸릴 시 커넥션 유지
private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs)
{
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
//닫을 때도 DataSourceUtils 사용
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
repository 작성하였다고 동작할 수 있는게 아니다. configuration 해야된다.
지금까지는 SpringConfig 에서MemoryMemberRepository를 bean 으로 등록하여 사용하고 있었다.
-> SpringConfig에 밑의 코드로 수정, 추가
실행(h2가 활성화 상태여야함!!)
결과
localhist:8080 -> 회원목록
DB에 저장했던 목록이 나오면 성공!
-> 회원 추가 후 회원목록 확인, h2 MEMBER Table도 확인해보기
데이터를 DB에 저장하기 때문에 스프링 서버를 다시 실행해도 데이터가 안전하게 저장된다.
MemberService 는 MemberRepository를 의존하고 있다.
MemberRepository는 구현체로 MemoryMemberRepository, JdbcMemberRepository가 있다.
Spring과 DB까지 연결하여 동작하는 통합테스트를 진행해보자
src/test/java/hello/hellospring/service/MemberServiceTest.java 파일 복붙 후 MemberServiceIntegrationTest.java 로 이름 변경 후 코드 작성..
테스트 케이스 할 때는 필드 기반의 @Autowired 사용하는 것이 제일 편함
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest //Spring 테스트 시 사용하는 어노테이션
@Transactional
class MemberServiceIntegrationTest {
//테스트 케이스 할 때는 필드 기반의 @Autowired 사용하는 것이 제일 편함
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
//Given
Member member = new Member();
member.setName("hello"); //중복 회원 가입이 되지 않게 수정
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
@Test
public void 중복_회원_예외() throws Exception {
//Given
Member member1 = new Member();
member1.setName("hello");
Member member2 = new Member();
member2.setName("hello");
//When
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class,
() -> memberService.join(member2));//예외가 발생해야 한다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
@SpringBootTest
스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional
테스트를 반복할 수 있어야 하는데 데이터가 남아있으면 오류가 발생한다.
데이터베이스는 기본적으로 트랜잭션이라는 개념이 있다 => insert 후 commit을 해야 DB에 반영된다.
@Transactional을 사용하면 테스트 끝나고 롤백해서 Db에서 테스트데이터를 없애주어 다음 테스트에 영향을 주지 않는다.
src/main/java/hello/hellospring/repository/JdbcTemplateMemberRepository.java
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
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 javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
//implements 중요
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
// @Autowired //생성자가 하나일 때는 Autowired 생략 가능
//injection을 받을 수 없기 때문에 DataSource를 인젝션 받아야 함 !!
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource); //dataSource 중요
} //spring이 dataSource를 자동으로 인젝션
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(new
MapSqlParameterSource(parameters));
member.setId(key.longValue());
//=> 쿼리를 짤 필요가 없다 : SimpleJdbcInsert, withTableName, usingGeneratedKeyColumns...
return member;
}
@Override
public Optional<Member> findById(Long id) { //Optional 중요
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id); //memberRowMapper: 결과가 나오는 것을 매핑
return result.stream().findAny();
} //JdbcMemberRepository - findById 와 비교하면 jdbcTemplate 사용하여 매우 간략하게 코딩할 수 있다는 것을 알 수 있음
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
//memberRowMapper: 결과가 나오는 것을 매핑
// private RowMapper<Member> memberRowMapper() {
// return new RowMapper<Member>() {
// @Override
// public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
// Member member = new Member();
// member.setId(rs.getLong("id"));
// member.setName(rs.getString("name"));
// return member;
// }
// };
// }
//위의 코드를 람다 스타일로 수정
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
}
SpringConfig 에 repository 조립
실행
스프링 테스트 환경을 만들었기 때문에 Application을 재실행하지 않고 테스트만 실행해서 확인이 가능하다.
=> MemberServiceIntegrationTest.java 만 실행!
build.gradle 에 JPA, h2 데이터베이스 관련 라이브러리 추가
spring-boot-starter-data-jpa는 내부에 jdbc 관련 라이브러리를 포함한다. 따라서 jdbc는 제거해도된다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
}
스프링부트에 JPA 추가
resources/application.properties
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
gradle 빌드 후 External Libraries 에 JPA와 hiebernate 확인
JPA 앤티티 매핑
src/main/java/hello/hellospring/domain/Member.java
@Entity : JPA가 관리하는 Entity
package hello.hellospring.domain;
import javax.persistence.*;
@Entity //Member 는 JPA가 관리하는 Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) //PK 매핑
private Long id;
// @Column(name = "username") //DB의 Column명이 username과 매핑
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
JPA 회원 리포지토리
pk 기반이 아닌 것들은 jpql을 작성해야한다.
src/main/java/hello/hellospring/repository/JpaMemberRepository.java
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import javax.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
private final EntityManager em;
//생성자
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
public Member save(Member member) {
em.persist(member); //persist : 저장
return member;
}
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id); //find(조회할 type, PK) : 조회
return Optional.ofNullable(member);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
//jpql 객체 지향 언어를 사용 하여야 한다.
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
}
서비스 계층에 트랜잭션 추가
JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
src/main/java/hello/hellospring/service/MemberService.java
JPA를 사용하도록 스프링 설정 변경
src/main/java/hello/hellospring/SpringConfig.java
기존 코드 주석 후 작성
실행
MemberServiceIntegrationTest.java
회원가입만 Run하면 성공 & 로그에 해당 문장 확인전체 Run -> 성공
스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술입니다. 따라서 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 한다.
앞의 JPA 설정 그대로 사용한다.
스프링 데이터 JPA 회원 리포지토리
인터페이스로 생성
src/main/java/hello/hellospring/repository/SpringDataJpaMemberRepository.java
interface 는 implements가 아니라 extends !!, 다중상속 가능
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
스프링 데이터 JPA 회원 리포지토리를 사용하도록 스프링 설정 변경
JpaRepository 를 사용하면 SpringDataJpaMemberRepository가 자동으로 구현체를 만들어서 스프링 Bean에 자동으로 등록해준다. => 우리는 그것을 가져다 쓰기만 하면 된다.
src/main/java/hello/hellospring/SpringConfig.java
package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
// //MemberRepository를 Jdbc...로 바꾸기 위한 작업
// private final DataSource dataSource;
// @Autowired
// public SpringConfig(DataSource dataSource) {
// this.dataSource = dataSource;
// }
// //
// //JPA 사용할 때
// private final DataSource dataSource;
// private final EntityManager em;
// public SpringConfig(DataSource dataSource, EntityManager em) {
// this.dataSource = dataSource;
// this.em = em;
// }
// //
//Spring-JPA 사용할 때
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
//스프링 빈 등록
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
//Spring-JPA 사용 할 때 전체 주석 걸기
// @Bean
// public MemberRepository memberRepository() {
//// return new MemoryMemberRepository();
//// return new JdbcMemberRepository(dataSource);
//// return new JdbcTemplateMemberRepository(dataSource);
// return new JpaMemberRepository(em);
// }
}
테스트 실행 -> 성공
스프링 데이터 JPA 제공 클래스
JpaRepository에 가면 자동으로 스프링데이터 JPA(findAll, save...)를 제공해주는 것을 확인할 수 있다.
스프링 데이터 JPA 제공 기능
실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는
라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적
쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를
사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.