2016年8月27日 星期六

Spring Data JPA (Spring Boot)

Spring Data JPA (Spring Boot)
以下將介紹如何在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/" , description = "帳戶管理" , produces = "application/json")
@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/" , description = "帳戶管理" , produces = "application/json")
@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/" , description = "帳戶管理" , produces = "application/json")
@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