前言
說到我們的web開發架構分層中,持久層是相對底層也是相對穩定的一層,奠定好根基后,我們才能專注於業務邏輯和視圖開發。而自從ORM思想蔓延開來后,全自動ORM的Hibernate和半自動ORM的MyBatis幾乎壟斷了持久層(當然還有很多公司或者大牛自己封裝的框架,不過相對占小部分),是發展過程中比較主流的兩款持久層框架。前段時間也關注了很多有關領域驅動設計的內容,感覺對前面的傳統架構分層沖擊較大,尤其是業務邏輯層、持久層、實體ORM那塊,引入了許多新概練,一時間也遇到了很多困惑,網上搜索資料發現領域驅動其實由來已久,目前也應用很多,但是想要完全掌握,並不是一件容易事。當然本文跟領域驅動並無直接關聯,現在的問題是在面試題“Hibernate和MyBatis的區別”背景下,我們在持久層還有第三種典型選擇嗎,其實是有的,那就是本文的主角,Spring Data Jpa。
介紹
說起Jpa,其實它並不是一個新概念,更不是說有了Spring Data Jpa才出現,它是Java Persistence API的簡稱,中文名Java持久層API,它是一種規范,例如Hibernate框架即實現了這種規范,Spring Data Jpa中便集成了Hibernate的模塊。Spring Data,看名字很容易知道又是Spring系列的,除了Spring MVC在web層的成功,在持久層這塊Spring也想拿下,大有想一統江湖的勢頭。另外去深入關注Spring Data內容,還會發現,不僅僅是RDBMS,還有Spring Data Redis、Spring Data Mongodb等等...本文內容主要是針對關系型數據庫,本人在使用過程中,最看好的還是其在通用持久化方面的簡易封裝,基於層層的泛型繼承,我們可以省略大量的簡單增刪改查方法的編碼,另外提供的通過特定格式方法命名簡化方法定義過程也很特別和好用。下面就基於Spring Data編寫一個單獨的簡單持久層實例來展現其使用過程。
准備環境
Eclipse + MySql + Maven
基於傳統幾大框架的開發目前已經相對成熟很多了,但是就實際工作開發環境中,筆者最強烈的感受有一點,配置!配置文件實在太多了!特別是多工程組合集成的時候,漫天飛的XML和properties真是讓人頭大。所以建議現在學習過程中一定要盡量搞懂配置中每段配置語句的含義,哪個參數有什么作用,這樣進入實際開發中才不會一時間無章可循。本文中配置盡量給出注釋來闡述含義。
在eclipse新建一個普通maven項目,quickstart類型,項目結構大致如下
pom.xml依賴如下
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- Spring 系列 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.2.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>4.2.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-jpa</artifactId> <version>1.9.1.RELEASE</version> </dependency> <!-- Hibernate系列 --> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>4.3.11.Final</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>4.3.11.Final</version> </dependency> <!-- MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> <version>1.4</version> </dependency> </dependencies>
Spring Data Jpa 告別CRUD
第一步、配置文件(當然實際開發中,我們不會將配置這樣集中在一個文件中,同時數據源配置等關鍵參數往往會通過properties文件去設置然后引入)
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- Spring的bean掃描機制,會根據bean注解例如@Service等去實例化相應bean --> <context:component-scan base-package="com.sdj"></context:component-scan> <!-- 這句代碼是告訴jpa我們的持久層接口都在哪些包下面 --> <jpa:repositories base-package="com.sdj.repository"/> <!-- 這里使用dbcp配置數據源,能實現連接池功能 --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://192.168.0.100:3306/sdj"/> <property name="username" value="root"/> <property name="password" value="abc123"/> </bean> <!-- 實體管理器工廠配置,關聯數據源,指定實體類所在包等等 --> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="packagesToScan" value="com.sdj.domain"/> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="database" value="MYSQL"/> <property name="generateDdl" value="false"/> <property name="showSql" value="true"/> </bean> </property> <property name="jpaProperties"> <props> <prop key="hibernate.hbm2ddl.auto">none</prop> <!-- 如果想要自動生成數據表,這里的配置是關鍵 --> <prop key="hibernate.dialect">org.hibernate.dialect.MySQL5InnoDBDialect</prop> <!-- <prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop> --> <prop key="hibernate.connection.charSet">UTF-8</prop> <prop key="hibernate.format_sql">true</prop> </props> </property> </bean> <!--配置事務管理器--> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory"/> </bean> <!--啟用事務注解來管理事務--> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
上面Spring配置文件中,實體管理器工廠配置是比較復雜的部分,下面具體到參數逐個介紹
dataSource,指定數據源
packagesToScan,與前面的component-scan類似,這里也是一種掃描機制,不過前面是掃描bean,這里既然是實體管理器,不難理解是掃描實體類,即指定實體類所在的包,這里為com.sdj.domain
jpaVendorAdapter,這里對應Jpa持久化實現廠商Hibernate,同時指定其專用特性,包括如下
database,指定數據庫,這里為MYSQL
generateDdl,是否自動生成DDL,這里為false
showSql,是否在控制台打印SQL語句,這點在調試時比較有用,能看到具體發送了哪些SQL
jpaProperties,jpa屬性設置,有如下
hibernate.hbm2ddl.auto,根據需要可以設置為validate | update | create | create-drop,當然也可以設置為none,設置的時候要小心,使用不到會有丟失數據的危險,例如這里如果我們想要根據實體類自動生成數據表,可以設置為update,不用的話這里設置為none
hibernate.dialect,指定數據庫方言,這里為MySql數據庫類型的
hibernate.connection.charSet,指定鏈接字符編碼,解決亂碼問題
hibernate.format_sql,前面指定控制台會打印SQL,這里是指定將其格式化打印,更加清晰美觀一點
第二步、實體類Person
package com.sdj.domain; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name="TB_PERSON") public class Person { private Integer id; //主鍵 private String name; //姓名 private String gender; //性別 private String addr; //地址 @Id @GeneratedValue(strategy=GenerationType.AUTO) public Integer getId() { return id; } @Column(name="NAME") public String getName() { return name; } public String getGender() { return gender; } public String getAddr() { return addr; } public void setId(Integer id) { this.id = id; } public void setName(String name) { this.name = name; } public void setGender(String gender) { this.gender = gender; } public void setAddr(String addr) { this.addr = addr; } }
如果仔細觀察實體中系列注解,會發現其來源是hibernate-jpa,這也是前面提到的hibernate實現jpa規范內容。常用注解解釋如下
@Entity,指定該類為一個數據庫映射實體類、
@Table,指定與該實體類對應的數據表
@Id和@Column,都是為實體類屬性關聯數據表字段,區別是Id是對應主鍵字段,另外還可以指定其對應字段名(不指定默認與屬性名一致)、長度等等...如果不加這兩個注解也是會以屬性名默認關聯到數據庫,如果不想關聯可以加上下面的@Transient
@GeneratedValue,指定主鍵生成策略
@Transient,表名該屬性並非數據庫表的字段映射
@OneToMany、@ManyToOne、@ManyToMany等,均為關聯映射注解系列,用於指定關聯關系,一對多、多對一等等
另外,@Id、@Column、@Transient等注解往往是加在屬性的get方法上。
第三步、持久層接口PersonRepository
package com.sdj.repository; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; import com.sdj.domain.Person; public interface PersonRepository extends JpaRepository<Person, Integer> { List<Person> findByName(String name); }
我們發現這里持久層代碼反而是最簡潔的,我們的注意點如下:
1.在這個針對Person實體的dao接口中我們並未定義常規通用的那些增刪改查等方法,只定義了一個特定的根據姓名查找人的方法,同時繼承了一個JpaRepository接口。
2.不管繼承接口也好,自定義方法也好,終究是接口,但是這里我們連實現類也沒有。
暫時先不走到業務邏輯Service層,一二三步走完,我們這個持久層程序已經可以運行了,下面我們編寫測試方法來看看。
package com.test; import java.util.List; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.sdj.domain.Person; import com.sdj.repository.PersonRepository; public class TestSDJ { @Test public void testDB() { @SuppressWarnings("resource") ApplicationContext context = new ClassPathXmlApplicationContext("application-root.xml"); PersonRepository bean = (PersonRepository) context.getBean("personRepository"); List<Person> list = bean.findAll(); System.out.println(list); } }
運行測試類可以看到控制台輸出如下結果
首先是格式化打印出了SQL語句,可以清楚看出來是查詢數據表所有記錄,下面則是輸出集合內容,這樣一來我們成功查出了表中數據。
疑問點如下:
1.首先前面我們定義PersonRepository是一個接口,並且沒有實現類,更沒有bean注解,那么通過Spring我們為什么能拿到這樣一個(接口名首字母小寫)名字的bean,這個bean又具體是什么?
2.我們的PersonRepository是一個接口,明沒有這樣的findAll()方法來查詢表中所有記錄,有人可能會很快想到其繼承了CrudRepository接口,那么這個方法又是怎么實現的?
JpaRepository這個接口是Spring Data提供的核心接口系列繼承鏈中的一環,主要有如下四個
Repository
public interface Repository<T, ID extends Serializable> { }
這是頂層接口,也是一個空接口,后面定義的泛型T對應我們的實體類,ID對應實體類中主鍵字段對應屬性的類型,比如本文是數字主鍵類型Integer,這里即對應Integer。
@NoRepositoryBean public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> { <S extends T> S save(S entity); T findOne(ID id); Iterable<T> findAll(); ... }
CrudRepository繼承Repository接口,CRUD大家應該都不陌生,增加(Create)、讀取查詢(Read)、更新(Update)和刪除(Delete),這里即新增了增刪改查等通用方法的定義。
public interface PagingAndSortingRepository<T, ID extends Serializable> extends CrudRepository<T, ID> { ... }
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> { ...... }
然后PagingAndSortingRepository繼承CrudRepository,同時新增分頁和排序等相關方法
最后就是文中的JpaRepository繼承PagingAndSortingRepository,同時定義了系列常用方法。
不知不覺,我們可以看到JpaRepository這里已經基本涵蓋了我們基礎操作的相關方法集合了,例如測試類中的查找所有記錄方法。但是問題沒完,方法再多,終究是接口,既然是接口,你這些方法沒有實現的話我們還是無法使用,但是我們在測試中已經發現成功了,那么它是怎么實現的呢。
我們來關注一下JpaRepository的實現類SimpleJpaRepository,源代碼類聲明段落如下
@Repository @Transactional(readOnly = true) public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> { ...... }
可以發現,這個類中已經實現了前面四環的定義方法,終於齊集五環。
@Repository,Spring系列bean注解之一,告訴系統這是一個持久層的bean示例;
@NoRepositoryBean,與之相反,使用該注解標明,此接口不是一個Repository Bean,例如前面的JpaRepository等,都用上了該注解,但是我們自定義的PersonRepository並沒有加,同時前面配置文件中 <jpa:repositories base-package="com.sdj.repository"/> ,隨后Spring Data會自動幫我們實現該接口,並實例出bean,bean名字為接口名首字母小寫后的字符串。
下面我們在原先的測試類中加入如下代碼
ApplicationContext context = new ClassPathXmlApplicationContext("application-root.xml"); String[] beanNames = context.getBeanDefinitionNames(); for(String beanName:beanNames) { System.out.println(beanName); } ...
重新運行測試類,我們除了能看到先前的輸出信息,在前面還會看到如下輸出
這一行行的都是Spring容器中現有的bean示例名稱,其中就有剛剛說到的"personRepository",所以我們才能根據這個名稱拿到該bean示例。
然后我們在Service層就可以注入持久層bean去組合業務邏輯操作了,通過@Autowired注入,同時不要忘記Service類上的@Service注解。
@Service public class PersonServiceImpl implements PersonService { @Autowired PersonRepository personRepository; ..... }
這樣一來,我們發現在常規的基礎操作范圍內,包括增刪改查、分頁查詢、排序等等,我們不用編寫一個方法,也不用寫一條SQL語句,Spring Data Jpa都幫我們封裝好了。但這只是一部分內容,例如前面接口中我們不是定義了一個findByName(),有人可能會說了,難不成這也能幫我們自動實現?就算能,那我再findByGenger()?到底能不能,這也引出了下面要說的內容。
Query creation 讓方法見名知意
在前面的測試類查詢方法改成如下:
List<Person> list = bean.findByName("小明");
運行測試方法,控制台輸出如下
看SQL語句發現的確是根據name姓名去查的,也成功查到了結果。
大家都知道增刪改查,一個查字一片天,簡單查也好,花樣查也好,它都是我們持久層不可缺少的部分。
除了前面提到了Spring Data對常規持久層操作的封裝,它另外還提供了一種通過格式化命名創建查詢的途徑,使得我們在創建查詢方法的時候有更多簡單的實現方式和選擇。
這里的格式具體體現示例如下:
查詢方法命名都是findBy****這樣的開頭,后面跟字段或者字段加關鍵字的組合
比如findByName等,相當於SQL:where name= ?都是規范的駝峰式命名。
比如findByNameAndGender,相當於SQL:where name= ? and gender = ?
這里的and就是一個keyword關鍵字,類似的還有許多,可以參考官方文檔鏈接點擊查看如下相關內容
也就是說符合上述命名規范的自定義方法,Spring Data同樣會幫助我們去實現這些方法,這樣一來又方便了許多。
但是如果我的命名不符合規范,我是否一定要實現這個接口並重寫相關方法,這樣其實也是可行的,不過Spring Data還提供了@Query方法注解,供我們自定義操作語句去實現它。例如上面的findByName方法,類似的我們在接口中新建一個任意方法名的如下方法:
@Query("from Person p where p.name = (:name)")
List<Person> abc(@Param("name")String name);
然后在測試類中引用該方法,能實現與findByName相同的查詢效果。
這里方法名給了個adc,同時方法上面注解定義了查詢語句,用過Hibernate的HQL語句的應該比較熟悉,這不禁讓人想起,當初Hibernate用的人用起來都說好啊好啊,面向對象思維啊,全自動啊,一句SQL都不用寫啊,真牛逼啊!然后全是HQL.....
Spring Data還有很多特性,如果有興趣也可以繼續深入學習一下。
小結
目前來看,除了主流的MyBatis、Hibernate,Spring Data Jpa也有不少公司在使用,而且Spring Boot系列中基於Spring Data的數據訪問也有使用到,畢竟Spring系列。個人比較喜歡Spring Data Jpa的點在數據庫通用操作的封裝,以及這些便利命名方法,這使得我們在業務邏輯相對簡單的情況下,能節省很多代碼和時間。但是問題是我們大多時候我們要攻克去專注的往往是那些復雜的業務邏輯,而在這點上Spring Data Jpa並無明顯優勢,莫非這就是在知乎上搜素"Spring Data怎么樣"連話題都搜不出來的原因....同時高封裝會不會引發低可控,如同以前用Hibernate,它自動幫助我們發送SQL,但是簡便的同時不會有像MyBatis那樣看到自己寫SQL的透明度來得直觀,同時SQL優化等東西似乎沒那么好掌控,這些應該是項目技術選型初始大致都會考慮到的一些問題吧,效率、性能、對開發人群的整體上手難度等等。總的來說,根據應用場景做出最適合項目的選擇才是關鍵吧。