以下將介紹如何在Spring Boot上使用Spring Data JPA
1.新增專案
2.勾選所需功能模組
其中REST Reopsitory非必要,REST Reopsitory主要可讓開發者可免撰寫Controller就可享有REST API可用
3.加入相依JAR檔案
以下2選1,步驟10介紹如何配置HikariCP
Maven
|
<dependency>
<groupId>com.h2database</groupId >
<artifactId>h2</artifactId >
<scope>runtime</scope >
</dependency>
<dependency>
<groupId com.zaxxer</groupId >
<artifactId>HikariCP</artifactId >
<version>2.4.7</version>
</dependency>
|
4.參數設定
application.properties
|
spring.datasource.driverClassName= com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?characterEncoding=UTF-8
spring.datasource.username= root
spring.datasource.password= password
|
5.Entity與Repositroy撰寫
為了展示方便,定義簡單點的Data Model
Entity Relationship Diagram(ERD)
Entity
org.iwlp.model.User
|
@Entity
@Table(name="user" )
public class User {
@Id
@GeneratedValue(strategy = GenerationType. IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "account")
private String account;
@Column(name = "password")
private String password;
@Column(name = "address")
private String address;
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;
}
public String getAccount() {
return account ;
}
public void setAccount(String account ) {
this.account = account;
}
public String getPassword() {
return password ;
}
public void setPassword(String password ) {
this.password = password;
}
public String getAddress() {
return address ;
}
public void setAddress(String address ) {
this.address = address;
}
}
|
org.iwlp.model.Product
|
@Entity
@Table(name="product" )
public class Product {
@Id
@GeneratedValue(strategy = GenerationType. IDENTITY)
private Long id;
@Column(name = "name")
private String name;
@Column(name = "description")
private String description;
@Column(name = "price")
private int price ;
@Column(name = "stock")
private int stock ;
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;
}
public String getDescription() {
return description ;
}
public void setDescription(String descript ) {
this.description = descript;
}
public int getPrice() {
return price ;
}
public void setPrice(int price) {
this.price = price;
}
public int getStock() {
return stock ;
}
public void setStock(int stock) {
this.stock = stock;
}
}
|
Repository
org.iwlp.repository.User
|
@RepositoryRestResource
public interface UserReopsitory extends PagingAndSortingRepository<User, Long>{
}
|
org.iwlp.repository.User
|
@RepositoryRestResource
public interface ProductRepository extends CrudRepository<Product, Long>{
}
|
6.Configuration
若想使用Spring Data Rest, 請用annoation @Import(RepositoryRestMvcConfiguration.class)
org.iwlp.SpringBootJapApplication
|
@SpringBootApplication
@Import(RepositoryRestMvcConfiguration.class)
public class SpringBootJapApplication {
public static void main(String[] args) {
SpringApplication. run(SpringBootJapApplication.class, args);
}
}
|
若有使用Spring Data Rest,啟動時進入http://127.0.0.1:8080/
可看到尚未撰寫Controller時,Spring Data Rest以自動幫你建立REST APIs
每個REST API Resource會根據extends CrudRepository和extends PagingAndSortingRepository有所不同
以下簡易的展示API
Resource User 第一頁所有records
http://ift.tt/2nqfopX
Resource User 中 id=1 record
http://ift.tt/2nqgZMo
若要自訂Spring Data Rest中Resource的名稱,請參考以下修改方式
org.iwlp.repository.User
|
@RepositoryRestResource (collectionResourceRel = "user", path = "user")
public interface UserReopsitory extends PagingAndSortingRepository<User, Long>{
}
|
觀看REST APIs
http://127.0.0.1:8080/
7.自訂Controller
利用@Autowired 注入Repository, Repository可用的method與extends PagingAndSortingRepository息息相關
PagingAndSortingRepository只有搜尋相關API,若為Repository extends CrudRepository,則會有CRUD APIS
org.iwlp.controller.UserController |
@Api(value = "/v1/" ,
@RestController
@RequestMapping (value = "/api/user")
public class UserController {
@Autowired
UserReopsitory repository;
@RequestMapping(method=RequestMethod. GET,value="/findById" )
@ApiImplicitParams({
@ApiImplicitParam(name = "id" , value = "使用者Id", required = true, defaultValue = "2" ,dataType = "long", paramType = "query")
})
public User search(
@RequestParam(value = "id" ) Long id
) {
return repository .findOne(id);
}
}
|
以Swagger UI檢視自訂的REST APIs(很遺憾Spring Data Rest自動產生的controller還沒研究如何整合swagger,所以看不到)
操作結果
8.Controller通用APIs
若是沒有用Spring Data Rest自動產生的controller,那有甚麼方式可以更快速的實作每個Table的CRUD呢?
其實每個Table功能不外乎就CRUD,只是Resource對象不同,因此可使用抽象類別撰寫好對應的CRUD APIs
再利用泛型取得指定的Resource進行操作,或是直接載入RESThub專案近來也可以(RESThub 也是透過繼承共用的CRUD類別)。
實作方式,首先寫個介面(Interface) RestController,規劃好對應的功能(CRUD),但不會此介面不會寫出實際運作方式(如何實現CRUD)
因為Interface僅是框架,讓實作的類別可被規範要做那些Method
參考REDThub source code
org.iwlp.controller
|
package org.iwlp.controller;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.io.Serializable;
import java.util.Set;
/**
* REST controller interface
*
* @param <T> Your resource POJO to manage, maybe an entity or DTO class
* @param <ID> Primary resource identifier at webservice level, usually Long or String
*/
public interface RestController<T, ID extends Serializable> {
/**
* Create a new resource<br />
* REST webservice published : POST /
*
* @param resource The resource to create
* @return CREATED http status code if the request has been correctly processed, with updated resource enclosed in the body, usually with and additional identifier automatically created by the database
*/
@RequestMapping(method = RequestMethod. POST)
@ResponseStatus(HttpStatus. CREATED)
@ResponseBody
T create( @RequestBody T resource );
/**
* Update an existing resource<br/>
* REST webservice published : PUT /{id}
*
* @param id The identifier of the resource to update, usually a Long or String identifier. It is explicitely provided in order to handle cases where the identifier could be changed.
* @param resource The resource to update
* @return OK http status code if the request has been correctly processed, with the updated resource enclosed in the body
* @throws NotFoundException
*/
@RequestMapping(value = "{id}", method = RequestMethod.PUT)
@ResponseBody
T update( @PathVariable ID id , @RequestBody T resource);
/**
* Find all resources, and return the full collection (plain list not paginated)<br/>
* REST webservice published : GET /?page=no
*
* @return OK http status code if the request has been correctly processed, with the list of all resource enclosed in the body.
* Be careful, this list should be big since it will return ALL resources. In this case, consider using paginated findAll method instead.
*/
@RequestMapping(method = RequestMethod. GET, params = "page=no" )
@ResponseBody
Iterable<T> findAll();
/**
* Find all resources, and return a paginated and optionaly sorted collection<br/>
* REST webservice published : GET /search?page=0&size=20 or GET /search?page=0&size=20&direction=desc&properties=name
*
* @param page Page number starting from 0. default to 0
* @param size Number of resources by pages. default to 10
* @param direction Optional sort direction, could be "asc " or "desc"
* @param properties Ordered list of comma separeted properies used for sorting resulats. At least one property should be provided if direction is specified
* @return OK http status code if the request has been correctly processed, with the a paginated collection of all resource enclosed in the body.
*/
@RequestMapping(method = RequestMethod. GET)
@ResponseBody
Page<T> findPaginated( @RequestParam(value = "page" , required = false, defaultValue = "1" ) Integer page,
@RequestParam(value = "size" , required = false, defaultValue = "10" ) Integer size,
@RequestParam(value = "direction" , required = false, defaultValue = "ASC" ) String direction,
@RequestParam(value = "properties" , required = false) String properties );
/**
* Find a resource by its identifier<br/>
* REST webservice published : GET /{id}
*
* @param id The identifier of the resouce to find
* @return OK http status code if the request has been correctly processed, with resource found enclosed in the body
* @throws NotFoundException
*/
@RequestMapping(value = "{id}", method = RequestMethod.GET)
@ResponseBody
T findById( @PathVariable ID id );
/**
* Find multiple resources by their identifiers<br/>
* REST webservice published : GET /?ids[]=
* <p/>
* example : /? ids[]=1&ids []=2&ids[]=3
*
* @param ids List of ids to retrieve
* @return OK http status code with list of retrieved resources. Not found resources are ignored:
* no Exception thrown. List is empty if no resource found with any of the given ids.
*/
@RequestMapping(method = RequestMethod. GET, params = "ids[]" )
@ResponseBody
Iterable<T> findByIds( @RequestParam(value = "ids[]" ) Set<ID> ids);
/**
* Delete all resources<br/>
* REST webservice published : DELETE /<br/>
* Return No Content http status code if the request has been correctly processed
*/
@RequestMapping(method = RequestMethod. DELETE)
@ResponseStatus(HttpStatus. NO_CONTENT)
void delete();
/**
* Delete a resource by its identifier<br />
* REST webservice published : DELETE /{id}<br />
* Return No Content http status code if the request has been correctly processed
*
* @param id The identifier of the resource to delete
* @throws NotFoundException
*/
@RequestMapping(value = "{id}", method = RequestMethod.DELETE)
@ResponseStatus(HttpStatus. NO_CONTENT)
void delete( @PathVariable ID id );
}
|
光有Interface只是空有殼,還必須有個抽象類別替interface做完絕大部分的CRUD
RESThub的RepositoryBasedRestController有些許問題,findPaginated與findByIds共用同個API path
因此這邊改寫RepositoryBasedRestController一些功能,如果每個Table的records都需要紀錄日期,還可自行加入searchByDateTime
org.iwlp.controller.CustomRepositoryBasedRestController
|
package org.iwlp.controller;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import org.itri.exception.NotFoundException;
import org.iwlp.controller.RestController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
public abstract class CustomRepositoryBasedRestController<T, ID extends Serializable, R extends PagingAndSortingRepository > implements RestController<T, ID> {
protected R repository;
protected T t;
protected Logger logger = LoggerFactory.getLogger(CustomRepositoryBasedRestController. class);
/**
* You should override this setter in order to inject your repository with @Inject annotation
*
* @param repository The repository to be injected
*/
public void setRepository(R repository ) {
this.repository = repository;
}
/**
* {@inheritDoc}
*/
@Override
public T create( @RequestBody T resource ) {
return (T)this.repository .save(resource);
}
/**
* {@inheritDoc}
*/
@Override
public T update( @PathVariable ID id , @RequestBody T resource) {
Assert. notNull(id, "id cannot be null" );
T retrievedResource = this.findById(id);
if (retrievedResource == null) {
throw new NotFoundException();
}
return (T)this.repository .save(resource);
}
/**
* {@inheritDoc}
*/
@Override
public Iterable<T> findAll() {
return repository.findAll();
}
/**
* {@inheritDoc}
*/
@Override
@RequestMapping(method=RequestMethod. GET,value="/paging" )
@ApiImplicitParams({
@ApiImplicitParam(name = "direction" , value = "direction", required = false, defaultValue = "ASC" ,dataType = "string", paramType = "query"),
@ApiImplicitParam(name = "properties" , value = "column name", required = false, defaultValue = "id" ,dataType = "string", paramType = "query")
})
public Page<T> findPaginated( @RequestParam(value = "page" , required = false, defaultValue = "1" ) Integer page,
@RequestParam(value = "size" , required = false, defaultValue = "10" ) Integer size,
@RequestParam(value = "direction" , required = false, defaultValue = "" ) String direction,
@RequestParam(value = "properties" , required = false) String properties ) {
Assert. isTrue(page > 0, "Page index must be greater than 0" );
Assert. isTrue(direction.isEmpty() || direction.equalsIgnoreCase(Sort.Direction.ASC.toString()) || direction.equalsIgnoreCase(Sort.Direction.DESC.toString()), "Direction should be ASC or DESC");
if(direction .isEmpty()) {
return this.repository.findAll(new PageRequest(page - 1, size));
} else {
Assert. notNull(properties);
return this.repository.findAll(new PageRequest(page - 1, size, new Sort(Sort.Direction.fromString( direction.toUpperCase()), properties.split( ","))));
}
}
/**
*
* @param columnName : 欄位名稱
* @return T
*/
@RequestMapping(method=RequestMethod. GET,value="/findLast" )
public @ResponseBody T findLastRecord(@RequestParam (value = "columnName", required = true , defaultValue = "id") String columnName) {
final PageRequest pageable = new PageRequest(
0, 1, new Sort(
new Order(Direction.DESC, columnName)
)
);
List<T> list = this.repository.findAll(pageable ).getContent();
if(list .size() > 0){
return (T) this.repository .findAll(pageable).getContent().get(0);
} else {
return null;
}
}
/**
* {@inheritDoc}
*/
@Override
public T findById( @PathVariable ID id ) {
T entity = (T)this.repository .findOne(id) ;
if (entity == null) {
throw new NotFoundException();
}
return entity ;
}
/**
* {@inheritDoc}
*/
@Override
public Iterable<T> findByIds( @RequestParam(value="ids[]" ) Set<ID> ids){
Assert. notNull(ids, "ids list cannot be null" );
return this.repository.findAll(ids );
}
/**
* {@inheritDoc}
*/
@Override
public void delete() {
Iterable<T> list = repository.findAll();
for (T entity : list) {
repository.delete(entity);
}
}
/**
* {@inheritDoc}
*/
@Override
public void delete(@PathVariable ID id) {
T resource = this.findById(id);
this.repository.delete( resource);
}
}
|
User Controller修改
org.iwlp.controller.UserController
|
@Api(value = "/v1/" ,
@RestController
@RequestMapping (value = "/api/user")
public class UserController extends CustomRepositoryBasedRestController<User, Long, UserReopsitory>{
@Autowired
@Override
public void setRepository(UserReopsitory repository){
this.repository = repository;
}
@RequestMapping(method=RequestMethod. GET,value="/findById" )
@ApiImplicitParams({
@ApiImplicitParam(name = "id" , value = "使用者Id", required = true, defaultValue = "2" ,dataType = "long", paramType = "query")
})
public User search(
@RequestParam(value = "id" ) Long id
) {
return repository .findOne(id);
}
}
|
使用Swagger UI檢視所有REST APIs
可看到繼承的CRUD REST APIs
9.Spring data實作更複雜的SQL
User Repository加入所需要的功能
可以使用Spring Data方式或用@Query的方式定義複雜的JPQL查詢資料
org.iwlp.repository.User
|
@RepositoryRestResource (collectionResourceRel = "user", path = "user")
public interface UserReopsitory extends PagingAndSortingRepository<User, Long>{
public User findFirstByNameAndAccount(String name, String account );
@Query( "SELECT u FROM User u WHERE u.name=?1 AND u.account=?2" )
public User search(String name, String account );
}
|
User Controller修改,加入自訂的APIs
org.iwlp.controller.UserController
|
@Api(value = "/v1/" ,
@RestController
@RequestMapping (value = "/api/user")
public class UserController extends CustomRepositoryBasedRestController<User, Long, UserReopsitory>{
@Autowired
@Override
public void setRepository(UserReopsitory repository){
this.repository = repository;
}
@RequestMapping(method=RequestMethod. GET,value="/findById" )
@ApiImplicitParams({
@ApiImplicitParam(name = "id" , value = "使用者Id", required = true, defaultValue = "2" ,dataType = "long", paramType = "query")
})
public User search(
@RequestParam(value = "id" ) Long id
) {
return repository .findOne(id);
}
}
|
最後再來檢視Swagger UI
10.改用Hikari DB Conn. Pool(非必要功能)
先修改Spring boot設定檔
application.properties
|
#Hikari
spring.datasource.driverClassName=com.mysql.jdbc.jdbc2.optional.MysqlDataSource
spring.datasource.username=root
spring.datasource.password=password
spring.dataSource.databaseName=testdb
spring.dataSource.portNumber=3306
spring.dataSource.serverName=localhost
|
在建立Datasource configuration,目的當然是要配置Hikari
org.iwlp.config
|
@Configuration
public class DataSourceConfig {
private static final Logger log = LoggerFactory.getLogger(DataSourceConfig. class);
@Value( "${spring.datasource.username}" )
private String user;
@Value( "${spring.datasource.password}" )
private String password;
@Value( "${spring.dataSource.portNumber}" )
private String portNumber;
@Value( "${spring.dataSource.serverName}" )
private String serverIp;
@Value( "${spring.dataSource.databaseName}" )
private String databaseName;
@Value( "${spring.datasource.driverClassName}" )
private String driverClassName;
@Bean
public DataSource primaryDataSource() {
log.debug("ClassName:{}" , driverClassName);
log.debug("ServerIp:{}" , serverIp);
log.debug("PortNumber:{}" , portNumber);
log.debug("user:{}" , user);
log.debug("password:{}" , password);
HikariConfig config = new HikariConfig();
config.setDataSourceClassName(driverClassName );
config.addDataSourceProperty("url" , "jdbc:mysql://" +serverIp+ ":"+portNumber +"/"+ databaseName+"?characterEncoding=UTF-8" );
config.addDataSourceProperty("user" , user);
config.addDataSourceProperty("password" , password);
config.addDataSourceProperty("cachePrepStmts" , "true");
config.addDataSourceProperty("prepStmtCacheSize" , "250");
config.addDataSourceProperty("prepStmtCacheSqlLimit" , "2048");
HikariDataSource ds = new HikariDataSource(config);
return ds ;
}
}
|
11.下載專案
參考資料
RESThub
Tags: Spring, Spring-Data-JPA, Spring Boot, IFTTT-SYNC
August 27, 2016 at 03:45PM
Open in Evernote