開始建立第三方 API 的 Model、Repository,提供新增、修改、刪除、讀取等功能,存入資料庫, EP 22-5 使用 JPA 增加範例 ,並透過 JUnit 5 來驗證產出結果。
Table of Contents
Toggle前言
Spring Data JPA 是基於 Hibernate 開發的 JPA 框架,簡化了 JPA 的寫法,可以在幾乎不用寫實作程式碼的情況下,實現對資料庫的存取操作,也能設定多個資料庫來源,使用多個資料表查詢,採用原生 SQL 自訂查詢等功能。
Java HttpClient JPA
檔案目錄
./
+- build.gradle
+- src
+- main
+- resources
| +- application.properties
+- java
+- org
+- ruoxue
+- spring_boot_168
+- config
| +- DataSourceConfig
+- game
| +- ggg
| +- model
| | +- User.java
| +- repository
| | +- UserRepository.java
設定
application.properties
顯示 SQL 語法:
spring.jpa.show-sql=true
DataSourceConfig.java
basePackages 增加:
org.ruoxue.spring_boot_168.game.ggg.repository
packages 增加:
org.ruoxue.spring_boot_168.game.ggg.model
package org.ruoxue.spring_boot_168.config;
import java.util.Map;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateProperties;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateSettings;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.orm.jpa.EntityManagerFactoryBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
@EnableTransactionManagement
@EnableJpaRepositories(entityManagerFactoryRef = "entityManagerFactory", //
transactionManagerRef = "transactionManager", //
basePackages = { "org.ruoxue.spring_boot_168.game.ggg.repository" //
})
@Configuration
@EnableConfigurationProperties
@Slf4j
public class DataSourceConfig extends DataSourceAutoConfiguration {
@Autowired
private HibernateProperties hibernateProperties;
@Autowired
private JpaProperties jpaProperties;
@Primary
@Bean(name = "dataSourceProperties")
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public DataSourceProperties dataSourceProperties() {
DataSourceProperties dataSourceProperties = new DataSourceProperties();
return dataSourceProperties;
}
@Primary
@Bean(name = "hikariConfig")
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariConfig hikariConfig() {
HikariConfig hikariConfig = new HikariConfig();
return hikariConfig;
}
@Primary
@Bean(name = "dataSource")
public DataSource dataSource(@Qualifier("hikariConfig") HikariConfig hikariConfig) {
log.info("poolName: " + hikariConfig.getPoolName());
log.info("jdbcUrl: " + hikariConfig.getJdbcUrl());
log.info("username: " + hikariConfig.getUsername());
log.info("driverClassName: " + hikariConfig.getDriverClassName());
log.info("maximumPoolSize: " + hikariConfig.getMaximumPoolSize());
log.info("connectionTimeout: " + hikariConfig.getConnectionTimeout());
log.info("maxLifetime: " + hikariConfig.getMaxLifetime());
log.info("autoCommit: " + hikariConfig.isAutoCommit());
log.info("jpaProperties: " + jpaProperties.getProperties());
DataSource dataSource = new HikariDataSource(hikariConfig);
return dataSource;
}
@Primary
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(EntityManagerFactoryBuilder builder,
@Qualifier("dataSource") DataSource dataSource) {
Map<String, Object> properties = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(),
new HibernateSettings());
return builder.dataSource(dataSource)//
.packages( //
"org.ruoxue.spring_boot_168.game.ggg.model" //
).persistenceUnit("primaryDatabase")//
.properties(properties)//
.build();
}
@Primary
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(
@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
@Primary
@Bean(name = "jdbcTemplate")
public JdbcTemplate jdbcTemplate(@Qualifier("dataSource") DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
實作
User.java
建立 Model,使用者存放帳號、密碼等資訊,表格名稱 game_ggg_user,可自定義,並且建立兩個唯一值欄位,使用 unique 索引。
package org.ruoxue.spring_boot_168.game.ggg.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import org.hibernate.annotations.GenericGenerator;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 使用者
*/
@Entity
@Table(name = "game_ggg_user", indexes = { @Index(name = "UK_game_ggg_user_cid", columnList = "cid", unique = true),
@Index(name = "IDX_game_ggg_user_name", columnList = "name", unique = true) })
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class User {
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid")
@Column(length = 32)
@Id
private String id;
/** 帳號 */
@Column(length = 30)
private String cid;
/** 名稱 */
@Column(length = 50)
private String name;
/** 密码 */
@Column(length = 32)
private String password;
/** 密码鹽 */
@Column(length = 32)
private String salt;
@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.JSON_STYLE);
builder.appendSuper(super.toString());
builder.append("cid", cid);
builder.append("name", name);
return builder.toString();
}
}
UserRepository.java
建立 Repository ,存取資料庫,繼承 PagingAndSortingRepository 介面,定義 findByCid,依帳號取得使用者,其他不需再任何實作,因為 JPA 已經實現功能。
package org.ruoxue.spring_boot_168.game.ggg.repository;
import org.ruoxue.spring_boot_168.game.ggg.model.User;
import org.springframework.data.repository.PagingAndSortingRepository;
/**
* 使用者儲存庫
*/
public interface UserRepository extends PagingAndSortingRepository<User, String> {
/**
* 依帳號取得使用者
*
* @param cid
* @return
*/
User findByCid(String cid);
}
測試 JUnit 5
UserRepositoryTest.java
新增單元測試,驗證是否符合預期 。
package org.ruoxue.spring_boot_168.game.ggg.repository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.ruoxue.spring_boot_168.Application;
import org.ruoxue.spring_boot_168.game.ggg.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class)
public class UserRepositoryTest {
@Autowired
private UserRepository repository;
@Test
public void repository() {
System.out.println(repository);
assertNotNull(repository);
}
@Test
public void insert() {
String value = "ruoxue";
User found = repository.findByCid(value);
if (found != null) {
return;
}
User user = new User();
user.setCid(value);
user.setName("player");
user.setPassword("");
user.setSalt("");
User result = repository.save(user);
System.out.println(result);
assertNotNull(result);
assertEquals(value, result.getCid());
}
@Test
public void findByCid() {
String value = "ruoxue";
User user = repository.findByCid(value);
System.out.println(user);
if (user != null) {
assertEquals(value, user.getCid());
}
}
@Test
public void update() {
String value = "ruoxue";
String name = "test_player";
User user = repository.findByCid(value);
if (user != null) {
user.setName(name);
User updated = repository.save(user);
System.out.println(user);
assertEquals(name, updated.getName());
}
}
@Test
public void delete() {
String value = "ruoxue";
User user = repository.findByCid(value);
if (user != null) {
repository.delete(user);
System.out.println(user);
}
}
}
insert
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
Hibernate: select user0_.id as id1_0_, user0_.cid as cid2_0_, user0_.name as name3_0_, user0_.password as password4_0_, user0_.salt as salt5_0_ from game_ggg_user user0_ where user0_.cid=?
Hibernate: insert into game_ggg_user (cid, name, password, salt, id) values (?, ?, ?, ?, ?)
{"cid":"ruoxue","name":"player"}
findByCid
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
Hibernate: select user0_.id as id1_0_, user0_.cid as cid2_0_, user0_.name as name3_0_, user0_.password as password4_0_, user0_.salt as salt5_0_ from game_ggg_user user0_ where user0_.cid=?
{"cid":"ruoxue","name":"player"}
update
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
Hibernate: select user0_.id as id1_0_, user0_.cid as cid2_0_, user0_.name as name3_0_, user0_.password as password4_0_, user0_.salt as salt5_0_ from game_ggg_user user0_ where user0_.cid=?
Hibernate: select user0_.id as id1_0_0_, user0_.cid as cid2_0_0_, user0_.name as name3_0_0_, user0_.password as password4_0_0_, user0_.salt as salt5_0_0_ from game_ggg_user user0_ where user0_.id=?
Hibernate: update game_ggg_user set cid=?, name=?, password=?, salt=? where id=?
{"cid":"ruoxue","name":"test_player"}
delete
測試方法上點右鍵執行 Run As -> JUnit Test ,查看 console。
Hibernate: select user0_.id as id1_0_, user0_.cid as cid2_0_, user0_.name as name3_0_, user0_.password as password4_0_, user0_.salt as salt5_0_ from game_ggg_user user0_ where user0_.cid=?
Hibernate: select user0_.id as id1_0_0_, user0_.cid as cid2_0_0_, user0_.name as name3_0_0_, user0_.password as password4_0_0_, user0_.salt as salt5_0_0_ from game_ggg_user user0_ where user0_.id=?
Hibernate: delete from game_ggg_user where id=?
{"cid":"ruoxue","name":"test_player"}
HeidiSQL
查看資料表結構,欄位名稱、資料類型、長度定義等。

查看新增資料,PK 、帳號、名稱等。

心得分享
實現了 CRUD 功能,及提供分頁、排序等常見功能,快速方便建立 DAO,現稱為 Repository,可以大幅減少開發時間,日後若要新增其他欄位,只需對 User 增加屬性,要是移除屬性,則需要到資料庫刪除欄位,複雜的查詢也可以使用原生的 SQL 語法來操作,當然方便開發不代表執行效率高,兩者要互相搭配,視情況而進行調整。