- java앱과 DB를 연결하기 위한 기술
- JDBC Driver는 여러타입의 DB와 연결할 수 있는 기능을 제공한다.
- Connection(연결) 을 생성하여 쿼리를 요청할 수 있는 상태를 만들어주고
- Statement(상태) 를 생성하여 쿼리를 요청하게 해주고
- ResultSet(결과셋) 을 생성해 쿼리 결과를 받아올 수 있게 해준다.
- 사용후에는 반드시!! close()를 호출하여 자원 해제를 시켜줘야한다.
@Test
@DisplayName("테이블 생성테스트")
void jdbcTest() throws SQLException {
// given
// docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass -e POSTGRES_USER=sanghoon -e POSTGRES_DB=messenger --name postgres_boot -d postgres
// docker exec -i -t postgres_boot bash
// su - postgres
// psql --username sanghoon --dbname messenger
// \list (데이터 베이스 조회)
// \dt (테이블 조회)
// IntelliJ Database 에서도 조회
String url = "jdbc:postgresql://localhost:5432/messenger";
String username = "sanghoon";
String password = "pass";
// when
try (Connection connection = DriverManager.getConnection(url, username, password)) {
try {
String creatSql = "CREATE TABLE ACCOUNT (id SERIAL PRIMARY KEY, username varchar(255), password varchar(255))";
try (PreparedStatement statement = connection.prepareStatement(creatSql)) {
statement.execute();
//원래는 Connection, PreParedStatement둘다 close()를 해야하는게 맞는데, try구문안에 넣어줌으로써, 자동으로 닫아줌
}
} catch (SQLException e) {
if (e.getMessage().equals("ERROR: relation \"account\" already exists")) {
System.out.println("ACCOUNT 테이블이 이미 존재합니다.");
} else {
throw new RuntimeException();
}
}
}
}
@Test
@DisplayName("JDBC 삽입/조회 실습")
void jdbcInsertSelectTest() throws SQLException {
// given
String url = "jdbc:postgresql://localhost:5432/messenger";
String username = "sanghoon";
String password = "pass";
// when
try (Connection connection = DriverManager.getConnection(url, username, password)) {
System.out.println("Connection created: " + connection);
String insertSql = "INSERT INTO ACCOUNT (id, username, password) VALUES ((SELECT coalesce(MAX(ID), 0) + 1 FROM ACCOUNT A), 'user1', 'pass1')";
try (PreparedStatement statement = connection.prepareStatement(insertSql)) {
statement.execute();
}
// then
String selectSql = "SELECT * FROM ACCOUNT";
try (PreparedStatement statement = connection.prepareStatement(selectSql)) {
var rs = statement.executeQuery();
while (rs.next()) {
System.out.printf("%d, %s, %s", rs.getInt("id"), rs.getString("username"),
rs.getString("password"));
}
}
}
}
@Test
@DisplayName("JDBC DAO 삽입/조회 실습")
void jdbcDAOInsertSelectTest() throws SQLException {
// given
AccountDAO accountDAO = new AccountDAO();
// when
var id =accountDAO.insertAccount(new AccountVO("new user", "new password"));
// then
var account = accountDAO.selectAccount(id);
assert account.getUsername().equals("new user");
}
public class AccountDAO {
//JDBC관련 변수
private Connection conn = null;
private PreparedStatement stmt = null;
private ResultSet rs = null;
private String url = "jdbc:postgresql://localhost:5432/messenger";
private String username = "sanghoon";
private String password = "pass";
// SQL쿼리
private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD)"
+"VALUES((SELECT coalesce(MAX(ID), 0) +1 FROM ACCOUNT A), ?, ?)";
private final String ACCOUNT_SELECT = "SELECT * FROM account WHERE ID = ?";
//CRUD 기능 메소드
public Integer insertAccount(AccountVO accountVO) throws SQLException {
var id = -1;
try{
String [] returnId = {"id"};
conn = DriverManager.getConnection(url,username,password);
stmt = conn.prepareStatement(ACCOUNT_INSERT,returnId);
stmt.setString(1,accountVO.getUsername());
stmt.setString(2,accountVO.getPassword());
stmt.executeUpdate(); // 따고 들어가보면 id가 아닌 count를 리턴
try(ResultSet rs = stmt.getGeneratedKeys()){
if(rs.next()){
id = rs.getInt(1);
}
}
}catch (SQLException sqlException){
sqlException.printStackTrace();
}
return id;
}
public AccountVO selectAccount(Integer id) throws SQLException{
AccountVO accountVO = null;
try{
conn = DriverManager.getConnection(url,username,password);
stmt = conn.prepareStatement(ACCOUNT_SELECT);
stmt.setInt(1,id);
var rs = stmt.executeQuery(); // 따고 들어가보면 id가 아닌 count를 리턴
if(rs.next()){
accountVO = new AccountVO();
accountVO.setId(rs.getInt("ID"));
accountVO.setUsername(rs.getString("USERNAME"));
accountVO.setPassword(rs.getString("PASSWORD"));
}
}catch (SQLException sqlException){
sqlException.printStackTrace();
}
return accountVO;
}
}
- 여기서 Connection객체 ~ PreparedStatement ~ ResultSet를 중복으로 쓰고 있기 때문에 따로 DAO파일로 빼고 작업한 것
- SQL 쿼리 요청시 중복 코드 발생
- DB별 예외에 대한 구분 없이 Checked Exception (SQL Exception) 처리
- Connection, Statement 등.. 자원 관리를 따로 해줘야함
- 안해주면 메모리 꽉차서 서버가 죽음
👍 QueryMapper의 등장 : JDBCTemplate , Mybatis
👍 ORM : JPA, Hibernate
- 쿼리 수행 결과와 객채 필드 매핑
- RowMapper 로 응답필드 매핑코드 재사용
- Connection, Statement, ResultSet 반복적 처리 대신 해줌
😵💫 But, 결과값을 객체 인스턴스에 매핑하는데 여전히 많은 코드가 필요함
- Dependency
// 4. SpringBoot 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
// 5. JDBC Template 등.. Spring 의존성을 받기위한 의존성
testImplementation 'org.springframework.boot:spring-boot-starter-test'
- TestCode
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
@DisplayName("SQL Mapper - JDBC Template 실습")
void sqlMapper_JDBCTemplateTest() throws SQLException {
// given
var accountTemplateDAO = new AccountTemplateDAO(jdbcTemplate);
// when
var id = accountTemplateDAO.insertAccount(new AccountVO("new user2", "new password2"));
// then
var account = accountTemplateDAO.selectAccount(id);
assert account.getUsername().equals("new user2");
}
- DAO
public class AccountTemplateDAO {
private final JdbcTemplate jdbcTemplate;
private final String ACCOUNT_INSERT = "INSERT INTO account(ID, USERNAME, PASSWORD)"
+"VALUES((SELECT coalesce(MAX(ID), 0) +1 FROM ACCOUNT A), ?, ?)";
private final String ACCOUNT_SELECT = "SELECT * FROM account WHERE ID = ?";
public AccountTemplateDAO(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
//CRUD 기능 메소드
public Integer insertAccount(AccountVO accountVO) throws SQLException {
KeyHolder keyHolder = new GeneratedKeyHolder();
//DB연결정보 넣어서 connection객체만듬 -> 그 연결 객체에 SQL쿼리문 넣어서 PrepaerStatement객체만듬, 거기에 필요한 저장값,파라미터 등의 값 set!
jdbcTemplate.update(con->{
var ps = con.prepareStatement(ACCOUNT_INSERT,new String[]{"id"});
ps.setString(1,accountVO.getUsername());
ps.setString(2,accountVO.getPassword());
return ps;
},keyHolder);
return (Integer) keyHolder.getKey();
}
public AccountVO selectAccount(Integer id) throws SQLException{
return jdbcTemplate.queryForObject(ACCOUNT_SELECT, new AccountRowMapper(),id);
}
}
- RowMapper
package jpa.pricehoon.jdbc.template;
import jpa.pricehoon.jdbc.vo.AccountVO;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class AccountRowMapper implements RowMapper<AccountVO> {
@Override
public AccountVO mapRow(ResultSet rs, int rowNum) throws SQLException {
var accountVO = new AccountVO();
accountVO = new AccountVO();
accountVO.setId(rs.getInt("ID"));
accountVO.setUsername(rs.getString("USERNAME"));
accountVO.setPassword(rs.getString("PASSWORD"));
return accountVO;
}
}
- 반복적인 JDBC 프로그래밍을 단순화
- SQL 쿼리들을 XML 파일에 작성하여 코드와 SQL 을 분리!
- 😵💫 But, 결국 SQL을 직접 작성하는것은 피곤하다…(DB 기능에 종속적) 😫
- 😵💫 But, 테이블마다 비슷한 CRUD 반복, DB타입 및 테이블에 종속적이다.
- MybatisTestCode
@SpringBootTest
@MybatisTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(DBConfiguration.class)
public class MyBatisTest {
// Mapper 클래스를 받으려면 mapper.xml 빌드 해야하고, 그러려면 main 으로 옮겨서 해야함...
@Autowired
AccountMapper accountMapper;// xml 로 쿼리관리
@Autowired
AccountMapperV2 accountMapperV2;// annotation 으로 쿼리관리
@Test
@DisplayName("SQL Mapper - MyBatis 실습(XML)")
void sqlMapper_MyBatisTest() {
// given
// when
accountMapper.insertAccount(new AccountMyBatisVO("new user3", "new password3"));
var account = accountMapper.selectAccount(1);
// then
assert !account.getUsername().isEmpty();
}
@Test
@DisplayName("SQL Mapper - MyBatis V2 실습(Annotation")
void sqlMapper_MyBatisV2Test() {
// given
// when
accountMapperV2.insertAccount(new AccountMyBatisVO("new user4", "new password4"));
var account = accountMapperV2.selectAccount(1);
// then
assert !account.getUsername().isEmpty();
}
}
- DBConfigureation
@Configuration
@MapperScan(basePackages = "jpa.pricehoon.mapper.*")
@EnableTransactionManagement
public class DBConfiguration {
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
final SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
// mapping xml 파일 위치를 bean mapper location 에 등록
PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(resolver.getResources("classpath:mappings/*.xml"));
return bean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
- MapperInterface 1
@Mapper
public interface AccountMapper {
AccountMyBatisVO selectAccount(@Param("id") int id);
void insertAccount(AccountMyBatisVO vo);
}
- MapperInterface 2
@Mapper
public interface AccountMapper {
AccountMyBatisVO selectAccount(@Param("id") int id);
void insertAccount(AccountMyBatisVO vo);
}
- 영속성?
- 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성을 말한다.
- 영속성을 갖지 않으면 데이터는 메모리에서만 존재하게 되고 프로그램이 종료되면 해당 데이터는 모두 사라지게 된다.
-그래서 우리는 데이터를 파일이나 DB에 영구 저장함으로써 데이터에 영속성을 부여한다.
persist(),merge()
>(영속성 컨텍스트에 저장된 상태)
>flush()
>(DB에 쿼리가 전송된 상태)
>commit()
>(DB에 쿼리가 반영된 상태)
Item item = new Item(); // 1
item.setItemNm("테스트 상품");
EntityManager em = entityManagerFactory.createEntityManager(); // 2
EntityTransaction transaction = em.getTransaction(); // 3
transaction.begin();
em.persist(item); // 4-1
em.flush(item). // 4-2 (DB에 SQL 보내기/commit시 자동수행되어 생략 가능함)
transaction.commit(); // 5
em.close(); // 6
1️⃣ 영속성 컨텍스트에 담을 상품 엔티티 생성
2️⃣ 엔티티 매니저 팩토리로부터 엔티티 매니저를 생성
3️⃣ 데이터 변경 시 무결성을 위해 트랜잭션 시작
4️⃣ 영속성 컨텍스트에 저장된 상태, 아직 DB에 INSERT SQL 보내기 전
5️⃣ 트랜잭션을 DB에 반영, 이 때 실제로 INSERT SQL 커밋 수행
6️⃣ 엔티티 매니저와 엔티티 매니저 팩토리 자원을 close() 호출로 반환
- 쓰기 지연이 발생하는 시점
- flush() 동작이 발생하기 전까지 최적화한다.
- flush() 동작으로 전송된 쿼리는 더이상 쿼리 최적화는 되지 않고, 이후 commit()으로 반영만 가능하다.
- 쓰기 지연 효과
- 여러개의 객체를 생성할 경우 모아서 한번에 쿼리를 전송한다.
- 영속성 상태의 객체가 생성 및 수정이 여러번 일어나더라도 해당 트랜잭션 종료시 쿼리는 1번만 전송될 수 있다.
- 영속성 상태에서 객체가 생성되었다 삭제되었다면 실제 DB에는 아무 동작이 전송되지 않을 수 있다.
- 즉, 여러가지 동작이 많이 발생하더라도 쿼리는 트랜잭션당 최적화 되어 최소쿼리만 날라가게된다.
💁♂️ 키 생성전략이
generationType.IDENTITY
로 설정 되어있는 경우 생성쿼리는 쓰기지연이 발생하지 못한다.
- why? 단일 쿼리로 수행함으로써 외부 트랜잭션에 의한 중복키 생성을 방지하여 단일키를 보장한다.
- 연관관계 구상
- gitHub : https://github.com/PriceHoon/jpaPrac
- 알아간 것
1) 간단한 것이지만 yml파일은 depth로 파이썬 처럼 인식한다.. 이것 땜에 2시간 고생함
2) 영속성 컨텍스트를 활용하여 쿼리문 없이 Select 함
@Test
void insertSelectGroupTest() {
// given
var newChannel = Channel.builder().name("new-group2").build();
// when
var savedChannel = channelRepository.insertChannel(newChannel);
// then
var foundUser = channelRepository.selectChannel(savedChannel.getId());//영속성 컨텍스트에서 조회하기 때문에 Select 쿼리 안나감
assert foundUser.getId().equals(savedChannel.getId());
}