Goal
- ์คํ๋ง JdbcTemplate ์ฌ์ฉํด๋ณด๊ธฐ
- SimpleJdbcInsert๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ ์ถ๊ฐํด๋ณด๊ธฐ
- ์คํ๋ง ๋ฐ์ดํฐ๋ฅผ ์ฌ์ฉํด์ JPA ์ ์ธํ๊ณ ์ฌ์ฉํด๋ณด๊ธฐ
์์ JDBC
Jdbc๋, Java DataBase Connectivity
, ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ์ฐ๊ฒฐํ๊ธฐ ์ํ API์ด๋ค.
Jdbc๋ฅผ ์ด์ฉํด select
์ฟผ๋ฆฌ ์์ฑํด๋ณด๋ฉด ์๋์ ๊ฐ๋ค.
@Override
public Account findById(String id) {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
String sql = "select id, name from Account where id = ?";
try {
conn = datasource.getConnection();
st = conn.prepareStatement(sql);
st.setString(1, id); // ?์๋ id๊ฐ ๋ค์ด๊ฐ๋ค.
...
} catch (SQLException e) {
...
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {}
}
if (st != null) { ... } // st.close
if (conn != null) { ... } // conn.close()
}
return null;
}
JDBC๋ฅผ ์ฌ์ฉํ๋ฉด DB ์ฐ๊ฒฐ๋ถํฐ ๋ง์ง๋ง์ ์ฐ๊ฒฐํด์ ๊น์ง ์ผ์ผํ ๋ค ํด์ค์ผํ๋ค.
์ด๋ฐ ๋ถํธํจ์ ํด์ํ๊ธฐ ์ํด Spring์์ ์ ๊ณตํ๋ JdbcTemplate
ํด๋์ค๊ฐ ์๋ค.
Spring์์ ์ ๊ณตํ๋ JdbcTemplate
- dependency ์ถ๊ฐ
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>
- JdbcTemplate
query("sql", RowMapper)
queryForObject("sql", RowMapper, Object... args)
update("sql", Object... args)
Object... args
๋ select * from Account where id = ?
์ ๊ฐ์ด ?์ ํด๋นํ๋ ๊ฐ์ ์ธ์๋ก ๋๊ฒจ์ฃผ๋ ๊ฒ์ด๋ค.
@Repository
public class JdbcAccountRepository implements AccountRepository{
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Long save(Account account) {
jdbcTemplate.update("insert into Account values(?, ?)",
account.getId(),
account.getUsername());
return account.getId();
}
@Override
public List<Account> findAll() {
return jdbcTemplate.query("select * from Account", this::rowToAccount);
}
@Override
public Account findById(Long id) {
return jdbcTemplate.queryForObject("select * from Account where id = ?",
this::rowToAccount,
id);
}
@Override
public Account findByUsername(String username) {
return jdbcTemplate.queryForObject("select * from Account where username = ?",
this::rowToAccount,
username);
}
private Account rowToAccount(ResultSet rs, int rowNum) throws SQLException {
return new Account(rs.getLong("id"),
rs.getString("username"));
}
}
์๋ ์ฒ๋ผ ์ต๋ช ํจ์๋ก ๋ง๋ค์ด๋ ๋๋ค.
jdbc.queryForObject(sql, new RowMapper<Account>() {
public Account mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Account(rs.getString("id"),
rs.getString("username"));
}
}
์๊น ์์ JDBC ์ฌ์ฉํ select
์ฟผ๋ฆฌ๋ ๋น๊ตํด๋ณด์.
์์ฒญ๋๊ฒ ํธ๋ฆฌํด์ก๋ค.
์์ Jdbc
์์๋ DB ์ปค๋ฅ์
์ ์์ฑํ๊ณ ํ๋ค๋ฉด, ์ด์ JdbcTemplate
์ ์ฌ์ฉํ ๋์๋ DataSource
๋ผ๋ ์ ๋ณด๋ฅผ ์ค์ ํด์ค์ผ ํ๋ค.
์๋ ๋๊ฐ์ง ๋ฐฉ๋ฒ์ ์ฌ์ฉํ ์ ์๋ค. (๋ ๋ง์์๋ ์๋ค.)
DataSource
๋ฅผ ์ง์ Bean
์ผ๋ก ๋ฑ๋กapplication.yml
์ ์ค์
DataSource๋ฅผ Bean์ผ๋ก ๋ฑ๋ก
@Configuration์ด ๋ถ์ Class ์๋์ ์์ฑํด์ค๋ค.
@Bean
public BasicDataSource source() {
BasicDataSource source = new BasicDataSource();
source.setUrl("jdbc:h2:tcp://localhost/~/databaseName");
source.setDriverClassName("org.h2.Driver");
source.setUsername("sa");
source.setPassword("");
return source;
}
@Bean
public JdbcTemplate dataSource(BasicDataSource source) {
return new JdbcTemplate(source);
}
๋น์ผ๋ก ์ฃผ์ ๋ฐ์์ ์ฌ์ฉํ๋ฉด ๋๋ค.
@Autowired
private JdbcTemplate jdbc;
application.yml ๋ฑ๋ก
- Spring boot์์ ์ ๊ณตํด์ฃผ๋ ๋ฐฉ๋ฒ์ด๋ค. ํธ๋ฆฌํ ๋ฐฉ๋ฒ์ด๋ค.
# DataSource ์ค์
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:tcp://localhost/~/prac_jdbc
username: sa
password:
# SQL ์์ฑ์ ์ํ ์ค์
sql:
init:
schema-locations: classpath:scheme.sql
mode: always
ํ ์คํธ ํด๋ณด๊ธฐ
- ๊ฐ๋จํ
Controller
์์ฑ
// AccountController.java
@RestController
public class AccountController {
@Autowired
private AccountRepository jdbcAccountRepository ;
@GetMapping("/account")
public List<Account> findAll() {
return jdbcAccountRepository.findAll();
}
@GetMapping("/account/{id}")
public Account findById(@PathVariable Long id) {
return jdbcAccountRepository.findById(id);
}
@PostMapping("/account")
public Long save(@RequestBody Account account) {
return jdbcAccountRepository.save(account);
}
}
์คํ๊ฒฐ๊ณผ
insert ๋ฌธ์ ์๋ต.
- GET
/account
- GET
/account/{id}
- POST
/account
์ฐ๊ด๊ด๊ณ ๋งคํ์ผ ๋, ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๋ 2๊ฐ์ง ๋ฐฉ๋ฒ (JdbcTemplate vs SimpleInsert)
JdbcTemplate.update
- ํธ๋ฆฌํ
SimpleInsert
JdbcTemplate ์ฌ์ฉ
- Account์ Article์ ์๋ก ์ฐ๊ด๊ด๊ณ๊ฐ ์๋ค๊ณ ๊ฐ์ ํ๋ค.
- ํ๋์ Account๋ ์ฌ๋ฌ ๊ฐ์ Article์ ๊ฐ์ง๋ค.
์ฐ๊ด๊ด๊ณ๊ฐ ์๋ ๋ ํ
์ด๋ธ์ ์ ์ฅํ๊ธฐ ์ํด PreparedStatementCreator
, KeyHolder
๋ฅผ ์ฌ์ฉํ๋ค.
public class Account {
...
List<Article> articles;
...
}
public class Article {
Long id;
String title;
Date createdAt;
...
}
@Repository
public class JdbcAccountRepository implements AccountRepository {
@Autowired
private JdbcTemplate jdbc;
public Long save(Account account) {
Long accountId = saveAccountInfo(account);
account.setId(accountId);
for (Article article : account.getArticles()) {
saveArticleToAccount(article, accountId);
}
return account.getId();
}
public Long saveAccountInfo(Account account) {
PreparedStatementCreator psc = new PreparedStatementCreatorFactory(
"insert into Account (name, age) values (?, ?)",
Types.VARCHAR, Types.INTEGER
).newPreparedStatementCreator(
Arrays.asList(
account.getUsername(),
account.getAge()
)
);
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(psc, keyHolder);
return keyHolder.getKeyAs(Long.class);
}
private void saveArticleToAccount(Article article, Long accountId) {
jdbcTemplate.update(
"insert into Account_Article (account, article) values (?, ?)",
accountId, article.getId());
}
}
- ์ฝ๋๊ฐ ๋ณต์กํ๋ค.
- ์์ฑ๋
Account ID
๊ฐ์ ์ป๊ธฐ ์ํด์๋KeyHolder
๊ฐ ํ์ํ๋ฐ, ์ดKeyHolder
๋ฅผ ์ฌ์ฉํ๊ธฐ ์ํด์๋ ๊ผญPreparedStatementCreator
๊ฐ์ฒด๊ฐ ํ์ํ๋ค.
SimpleJdbcInsert ์ฌ์ฉ
JdbcTemplate
๋ํผ ํด๋์ค์ด๋ค.
SimpleAccountRepository.java
@Repository
public class SimpleAccountRepository implements AccountRepository{
private final ObjectMapper objectMapper;
private final SimpleJdbcInsert accountInserter;
private final SimpleJdbcInsert accountArticleInserter;
public SimpleAccountRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
this.accountInserter = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("Account")
.usingGeneratedKeyColumns("id");
this.accountArticleInserter = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("Account_Articles");
this.objectMapper = objectMapper;
}
@Override
public Long save(Account account) {
Long accountId = saveAccountInfo(account);
account.setId(accountId);
for (Article article : account.getArticles()) {
saveArticleToAccount(article, accountId);
}
return null;
}
private void saveArticleToAccount(Article article, Long accountId) {
// key ๊ฐ์ ํ
์ด๋ธ์ ์ด ์ด๋ฆ๊ณผ ๋์ผ
HashMap<String, Object> values = new HashMap<>();
values.put("article", article.getId());
values.put("account", accountId);
accountArticleInserter.execute(values);
}
private Long saveAccountInfo(Account account) {
Map<String, Object> values = objectMapper.convertValue(account, Map.class);
return (Long) accountInserter.executeAndReturnKey(values);
}
...
}
SimpleJdbcInsert
๋ ๋์ฑ ํธ๋ฆฌํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ค.- ์ด์ ์๋ key๋ฅผ ๋๋ ค๋ฐ๊ธฐ ์ํด
KeyHolder
+PrepareStatementCreator
๋ฅผ ์ฌ์ฉํ๋ค๋ฉด, ์ด์ ๋SimpleJdbcInsert
ํด๋์ค์executeAndReturnkey
๋ฅผ ์ด์ฉํด ํค๋ฅผ ๋๋ ค๋ฐ์ ์ ์๋ค.
Spring Data JPA ์ฌ์ฉ
Spring Jdbc
๋ณด๋ค ํจ~์ฌ ํธ๋ฆฌํ ๊ธฐ๋ฅ๋ค ์ ๊ณต
dependency
์ถ๊ฐ
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
- ๊ฐ๋จํ CRUD ๊ตฌํ
Repository
๋interface
๋ฅผ ์์ํ๋ค.
JpaRepository ์ธํฐํ์ด์ค๋ฅผ ์์๋ฐ๋๋ค.
@Repository
public interface JpaAccountRepository extends JpaRepository<Account, Long> {
}
์ด์ Account
๋ฅผ Jpa Entity
๋ก ๋ฑ๋กํ๊ธฐ ์ํด์ ๋ค์ 3๊ฐ์ง๊ฐ ํ์ํ๋ค.
- Jpa Entity์ธ ๊ฒ์ ์๋ ค์ฃผ๊ธฐ ์ํด ํด๋์ค ๋ ๋ฒจ์
@Entity
์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ค๋ค. - ๊ธฐ๋ณธ ์์ฑ์ (๋งค๊ฐ๋ณ์๊ฐ ์๋ ์์ฑ์)
- id ํ๋์
@Id
์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ค๋ค. @GeneratedValue(strategy=GenerationType.****)
๋ฅผ ๊ฐ์ด ์ฌ์ฉํ๋ค.- ๊ธฐ๋ณธํค ์์ฑ์๋ 4๊ฐ์ง ์ ๋ต์ด ์๋ค.
IDENTITY
: ๊ธฐ๋ณธํค๋ฅผ null๋ก ๋ฃ์ผ๋ฉด, DB๊ฐ ์์์ ์์ฑํด์ ๋ฃ์ด์ค๋ค. (ex: Mysql, PostgreSQL,,)SEQUENCE
: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํ์ค๋ ์ ์ผํ ๊ฐ์ ์์๋๋ก ์์ฑํ๋ค. (ex. Oracle, PostgreSQL, H2..)TABLE
: ํค ์์ฑ ์ ์ฉ ํ ์ด๋ธ์ ํ๋ ๋ง๋ค์ด์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ํ์ค๋ฅผ ํ๋ด๋ด๋ ์ ๋ตAUTO
: ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฐฉ์ธ(dialect)๋ง๋ค ์ ์ธ๊ฐ์ง ์ ๋ต์ ์๋์ผ๋ก ์ง์ ํ๋ค.
@Data
@RequiredArgsConstructor
@Builder
@Entity
public class Account {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String username;
private int age;
@OneToMany(mappedBy = "account")
private List<Article> articles;
}
Account
์ ์ผ๋๋ค ๋งคํ์ด ๋์ด์๋ Article
๋ Entity
๋ก ๋ฑ๋กํด์ค๋ค.
- Account(1) ์
์ฅ์์๋ Article(N)์
๋ค(N)
์ด๊ธฐ ๋๋ฌธ์,@ManyToOne
์ ๋ถ์ฌ์ค๋ค. @ManyToOne
์๋ ํญ์ JoinColumn์ผ๋ก1(์ผ)
์ ํด๋นํ๋ Entity๋ฅผ ์ง์ ํด์ค์ผํ๋ค.@JoinColumn
(์๋ต๊ฐ๋ฅ) :EntityName + _ + ID
, ์๋ตํ๋ฉด JPA๊ฐ ์์์ ๊ตฌ๋ฌธ ๋ถ์์ํด์ ๋งคํํด์ค๋ค.
@Data
@Builder
@NoArgsConstructor
@Entity
public class Article {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String title;
private Date date;
@ManyToOne
@JoinColumn(name = "ACCOUNT_ID") // ์๋ต๊ฐ๋ฅ
private Account account;
}
AccountService ์์ฑ
@Service
public class Account1Service {
@Autowired
private JpaAccountRepository accountRepository;
public Account save(Account account) {
return accountRepository.save(account);
}
public List<Account> findAll() {
return accountRepository.findAll();
}
public Account findById(Long id) {
return accountRepository.findById(id)
.orElseThrow(IllegalStateException::new);
}
public Account update(Long id, Account account) {
Account oldAccount = this.findById(id);
if(account.getUsername() != null) {
oldAccount.setUsername(account.getUsername());
}
if (account.getAge() != 0) {
oldAccount.setAge(account.getAge());
}
return accountRepository.save(oldAccount);
}
public void delete(Long id) {
accountRepository.deleteById(id);
}
}
CRUD ํ ์คํธ
TEST ์ฝ๋
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class Account1ServiceTest {
@Autowired
private Account1Service accountService;
@Test
@Order(1)
void account_์ ์ฅ() {
for (int i = 1; i <= 3; i++) {
Account account = Account.builder()
.username("name" + i)
.age(20 + i)
.build();
Account save = accountService.save(account);
assertEquals(save.getUsername(), account.getUsername());
assertEquals(save.getAge(), account.getAge());
}
}
@Test
@Order(2)
void ์ ์ฒด_account_์กฐํ() {
List<Account> accounts = accountService.findAll();
assertEquals(accounts.size(), 3);
}
@Test
@Order(3)
void ํ๋์_account_์กฐํ() {
Account account = accountService.findById(1L);
assertEquals(account.getId(), 1L);
assertEquals(account.getUsername(), "name1");
assertEquals(account.getAge(), 21);
}
@Test
@Order(4)
void account_์
๋ฐ์ดํธ() {
Account account = Account.builder()
.username("user100")
.age(100)
.build();
Account update = accountService.update(2L, account);
assertEquals(update.getUsername(), account.getUsername());
assertEquals(update.getAge(), account.getAge());
}
@Test
@Order(5)
void account_์ญ์ () {
accountService.delete(3L);
assertThrows(IllegalStateException.class, () -> accountService.findById(3L));
}
}