最近Team開始嘗試使用Spring Boot + Spring Data JPA做爲數據層的解決方案,在網上逛了幾圈以後發現你們並不待見JPA,理由是(1)MyBatis簡單直觀夠用,(2)以Hibernate爲底層的Spring Data JPA複雜且性能通常。css
可是當咱們來到Spring Boot的世界後發現,相較於Spring Data JPA,MyBatis對Spring Boot的支持有限,Spring Data JPA與Spring Boot結合可讓dao變得很是簡單,好比(1)JPA自帶分頁對象,無需設置插件;(2)一個空接口搞定全部基本CRUD。java
本着虛心學習的態度,我決定將Spring Boot、Spring Data JPA和Druid三者整合在一塊兒,並分別對SQL Server和MySQL進行支持,但願本文可以幫助到須要相關技術的同窗。mysql
1. 程序和版本git
Spring Boot 2.0.4github
mssql-jdbc 6.2.2.jre8web
mysql-connector-java 5.1.46spring
druid-spring-boot-starter 1.1.10sql
2. properties配置文件數據庫
咱們把主程序配置文件application.properties和數據庫配置文件分開,這樣可以使application.properties不至於臃腫。app
(1) application.properties
1 server.port=9006 2 spring.application.name=spring-data-jpa 3 4 #Serialize JPA entity to Json string. 5 spring.jackson.serialization.fail-on-empty-beans=false
第5行的做用是避免com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer and no properties discovered to create BeanSerializer,該配置只對MSSQL數據源有效。
(2) db.properties
1 #Data source 1 2 db1.sqlserver.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver 3 db1.sqlserver.url=${DB1_URL:jdbc:sqlserver://127.0.0.1:1433;DatabaseName=MyTestDb1} 4 db1.sqlserver.username=${DB1_UID:tester} 5 db1.sqlserver.password=${DB1_PWD:tester}
6 db1.sqlserver.initial-size=1 7 db1.sqlserver.min-idle=1 8 db1.sqlserver.max-active=20 9 db1.sqlserver.max-wait=60000 10 db1.sqlserver.time-between-eviction-runs-millis=60000 11 db1.sqlserver.min-evictable-idle-time-millis=300000 12 db1.sqlserver.validation-query=select 1 13 db1.sqlserver.test-on-borrow=true 14 db1.sqlserver.test-While-Idle=true 15 db1.sqlserver.test-on-return=false 16 db1.sqlserver.pool-prepared-statements=false 17 db1.sqlserver.max-pool-prepared-statement-per-connection-size=20 18 19 db1.sqlserver.filter.stat.enabled=true 20 db1.sqlserver.filter.stat.db-type=mssql 21 db1.sqlserver.filter.stat.log-slow-sql=true 22 db1.sqlserver.filter.stat.slow-sql-millis=2000 23 24 db1.sqlserver.jpa.hibernate.dialect=org.hibernate.dialect.SQLServerDialect 25 db1.sqlserver.jpa.hibernate.show_sql=true 26 db1.sqlserver.jpa.hibernate.format_sql=true 27 28 #Data source 2 29 db2.mysql.driver-class-name=com.mysql.jdbc.Driver 30 db2.mysql.url=${DB2_URL:jdbc:mysql://127.0.0.1:3306/Test}?useUnicode=true&useSSL=false 31 db2.mysql.username=${DB2_UID:tester} 32 db2.mysql.password=${DB2_PWD:tester} 33 db2.mysql.initial-size=1 34 db2.mysql.min-idle=1 35 db2.mysql.max-active=20 36 db2.mysql.max-wait=60000 37 db2.mysql.time-between-eviction-runs-millis=60000 38 db2.mysql.min-evictable-idle-time-millis=300000 39 db2.mysql.validation-query=select 1 40 db2.mysql.test-on-borrow=true 41 db2.mysql.test-While-Idle=true 42 db2.mysql.test-on-return=false 43 db2.mysql.pool-prepared-statements=false 44 db2.mysql.max-pool-prepared-statement-per-connection-size=20 45 46 db2.mysql.filter.stat.enabled=true 47 db2.mysql.filter.stat.db-type=mysql 48 db2.mysql.filter.stat.log-slow-sql=true 49 db2.mysql.filter.stat.slow-sql-millis=2000 50 51 db2.mysql.jpa.hibernate.dialect=org.hibernate.dialect.MySQLDialect 52 db2.mysql.jpa.hibernate.show_sql=true 53 db2.mysql.jpa.hibernate.format_sql=true 54 db2.mysql.jpa.hibernate.enable_lazy_load_no_trans=true
該配置文件可分爲三部分:一是JPA的數據源基本信息配置(行5以前);二是JPA的數據庫鏈接池配置(行6-行17);三是Druid鏈接池的特殊配置(行19-行22);四是自定義配置(行24-行26)。
須要注意行54的配置,加這一行是爲了解決由Hibernate懶加載引發的異常org.hibernate.LazyInitializationException: could not initialize proxy [devutility.test.database.springdatajpa.dao.mysql.entity.Customer#100000123] - no Session
可是讓enable_lazy_load_no_trans=true會帶來必定的性能問題,具體參考https://vladmihalcea.com/the-hibernate-enable_lazy_load_no_trans-anti-pattern/
此外,解決org.hibernate.LazyInitializationException異常還有另一種方法,在每一個Entity類型上添加@Proxy(lazy = false)註解,經測試有效。
3. Java Config
爲便於管理,每一個數據源一個配置類,此處只列出一個數據源:
1 import java.util.Properties; 2 3 import javax.sql.DataSource; 4 5 import org.springframework.boot.context.properties.ConfigurationProperties; 6 import org.springframework.context.annotation.Bean; 7 import org.springframework.context.annotation.Configuration; 8 import org.springframework.context.annotation.Primary; 9 import org.springframework.context.annotation.PropertySource; 10 import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 11 import org.springframework.orm.jpa.JpaTransactionManager; 12 import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean; 13 import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter; 14 import org.springframework.transaction.PlatformTransactionManager; 15 16 import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder; 17 18 import devutility.internal.util.PropertiesUtils; 19 20 @Configuration 21 @PropertySource("classpath:db.properties") 22 @EnableJpaRepositories(basePackages = "devutility.test.database.springdatajpa.dao.mssql", entityManagerFactoryRef = "entityManagerFactory1", transactionManagerRef = "transactionManager1") 23 public class DataSource1Configuration { 24 @Primary 25 @Bean 26 @ConfigurationProperties("db1.sqlserver") 27 public DataSource dataSource1() { 28 return DruidDataSourceBuilder.create().build(); 29 } 30 31 @Bean 32 @ConfigurationProperties("db1.sqlserver.jpa") 33 public Properties jpaProperties1() { 34 return new Properties(); 35 } 36 37 @Primary 38 @Bean 39 public LocalContainerEntityManagerFactoryBean entityManagerFactory1() { 40 LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean(); 41 localContainerEntityManagerFactoryBean.setDataSource(dataSource1()); 42 localContainerEntityManagerFactoryBean.setPackagesToScan(new String[] { "devutility.test.database.springdatajpa.dao.mssql.entity" }); 43 localContainerEntityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter()); 44 localContainerEntityManagerFactoryBean.setJpaPropertyMap(PropertiesUtils.toMap(jpaProperties1())); 45 return localContainerEntityManagerFactoryBean; 46 } 47 48 @Bean 49 public PlatformTransactionManager transactionManager1() { 50 JpaTransactionManager transactionManager = new JpaTransactionManager(); 51 transactionManager.setEntityManagerFactory(entityManagerFactory1().getObject()); 52 return transactionManager; 53 } 54 }
4. Druid控制檯頁面配置
Druid的詳細配置見Druid官網
若是你不想對Druid控制檯的訪問加以限制能夠忽略此節,若是你但願經過用戶名和密碼訪問Druid控制檯,有以下兩種配置方式:
(1)Java Config
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; @Configuration public class DruidConfiguration { @Bean public ServletRegistrationBean<StatViewServlet> druidStatViewServlet() { ServletRegistrationBean<StatViewServlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); servletRegistrationBean.addInitParameter("loginUsername", "admin"); servletRegistrationBean.addInitParameter("loginPassword", "admin"); servletRegistrationBean.addInitParameter("resetEnable", "false"); return servletRegistrationBean; } @Bean public FilterRegistrationBean<WebStatFilter> druidStatFilter() { FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(new WebStatFilter()); filterRegistrationBean.setName("DruidWebStatFilter"); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } }
(2). 在application.properties文件中添加
#Configuration for druid spring.datasource.druid.stat-view-servlet.enabled=true spring.datasource.druid.stat-view-servlet.url-pattern=/druid/* spring.datasource.druid.stat-view-servlet.login-username=admin spring.datasource.druid.stat-view-servlet.login-password=admin
5. 應用
配置好以後就該實現CRUD的基本功能了:
(1) 定義一個實體類Customer
import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "Customer") public class Customer extends BaseEntity { @Id private long id; @Column(name = "Name1") private String name; @Column(name = "Address1") private String address; private String city; private int state; private int zip; private String phone; private String email;
這裏須要注意如下幾點:
a. 全部JPA的實體類都須要有@Entity的註解;
b. @Table註解可選,若是不設置則表名=類名,若是表名和類名不一致則須要配置;
c. @Column註解可選,用於表中字段名和實體類的屬性不一致的狀況;
d: 可在擁有@Id字段上添加@GeneratedValue註解用於生成主鍵。
(2) Dao層
a. 對於每個表,只須要定義一個簡單的接口並繼承JpaRepository<T, ID>便可實現基本的CRUD還有分頁操做:
package devutility.test.database.springdatajpa.dao.mysql; import org.springframework.data.jpa.repository.JpaRepository; import devutility.test.database.springdatajpa.dao.mysql.entity.Customer; public interface CustomerRepository extends JpaRepository<Customer, Long> { }
b. 假設你的實體類是經過聯表查詢獲得的,或者對於一個單表來講基本的CRUD沒法知足你的需求,你能夠經過使用@Query註解來手寫SQL語句實現,下面咱們來演示一下這種狀況:
首先定義一個實體類SimpleCustomer,該實體類只包含Customer的部分字段。
package devutility.test.database.springdatajpa.dao.mysql.entity; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "Customer") public class SimpleCustomer { @Id private long id; private String name; 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; } }
而後咱們再定義SimpleCustomer對應的Repository:
package devutility.test.database.springdatajpa.dao.mysql; import java.util.Date; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import devutility.test.database.springdatajpa.dao.mysql.entity.SimpleCustomer; public interface SimpleCustomerRepository extends JpaRepository<SimpleCustomer, Long> { @Query(value = "select ID, Name1 Name, Address1 Address, Created from Customer where Created > ?1 and Name1 is not null order by Created desc limit ?2, ?3", nativeQuery = true) List<SimpleCustomer> paging(Date startDate, int skip, int pageSize); }
在SimpleCustomerRepository中,咱們定義了一個接口paging,用來進行分頁查詢。注意,必定要有nativeQuery = true,不然報錯。
(3) 應用層
接下來就是怎樣使用上面定義的Repository了:
import java.text.ParseException; import java.util.Date; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import devutility.internal.models.OperationResult; import devutility.internal.text.format.DateFormatUtils; import devutility.test.database.springdatajpa.dao.mysql.CustomerRepository; import devutility.test.database.springdatajpa.dao.mysql.SimpleCustomerRepository; import devutility.test.database.springdatajpa.dao.mysql.entity.Customer; import devutility.test.database.springdatajpa.dao.mysql.entity.SimpleCustomer; @RestController @RequestMapping("/mysql") public class MySqlController { private int pageSize = 10; @Autowired private CustomerRepository customerRepository; @Autowired private SimpleCustomerRepository simpleCustomerRepository; @RequestMapping("/customer") public Customer findCustomer(String id) { return customerRepository.getOne(id); } @RequestMapping("/update-customer") public OperationResult updateCustomer(String id) { OperationResult result = new OperationResult(); Customer customer = customerRepository.getOne(id); if (customer == null) { result.setErrorMessage(String.format("Customer with id %d not found!", id)); return result; } customer.setName("Test-Customer"); Customer updatedCustomer = customerRepository.save(customer); result.setData(updatedCustomer); return result; } @RequestMapping("/paging-customers") public List<Customer> pagingCustomers(int page) { Pageable pageable = PageRequest.of(page, pageSize, Sort.by(Direction.DESC, "Created")); Page<Customer> customerPage = customerRepository.findAll(pageable); System.out.println(String.format("TotalElements: %d", customerPage.getTotalElements())); System.out.println(String.format("TotalPages: %d", customerPage.getTotalPages())); return customerPage.getContent(); } @RequestMapping("/paging-simple-customers") public List<SimpleCustomer> pagingSimpleCustomers(int page) throws ParseException { Date startDate = DateFormatUtils.parse("2018-01-01", "yyyy-MM-dd"); return simpleCustomerRepository.paging(startDate, (page - 1) * pageSize, pageSize); } }
除此以外,save方法也用於新增,delete方法用於刪除,再也不贅述。