文章目錄
引言
在接口測試中把Case
存儲至數據庫中,是比較常見的“數據驅動”做法。而在實際的接口測試用例開發中,對數據庫的操作無非就是“增刪改查”。就爲最普遍的單表操作而言,除了表和字段不同外,語句都是類似的,測試人員需要寫大量類似而枯燥的語句來完成業務邏輯。
爲了解決這些大量枯燥的數據庫操作語句,我們第一個想到的使用ORM
框架,比如:Hibernate
。通過整合Hibernate
之後,我們以操作Java實體的方式最終將數據改變映射到數據庫表中。
爲了解決抽象各個Java
實體基本的“增刪改查”操作,我們通常會以泛型的方式封裝一個模板Dao
來進行抽象簡化,但是這樣依然不是很方便,我們需要針對每個實體編寫一個繼承自泛型模板Dao
的接口,再編寫該接口的實現。雖然一些基礎的數據訪問已經可以得到很好的複用,但是在代碼結構上針對每個實體都會有一堆Dao
的接口和實現。
由於模板Dao
的實現,使得這些具體實體的Dao
層已經變的非常“薄”,有一些具體實體的Dao
實現可能完全就是對模板Dao
的簡單代理,並且往往這樣的實現類可能會出現在很多實體上。Spring-data-jpa
的出現正可以讓這樣一個已經很“薄”的數據訪問層變成只是一層接口的編寫方式。
Spring-data-jpa介紹
JPA是什麼?
JPA(Java Persistence API
)是Sun
官方提出的Java
持久化規範。它爲Java
開發人員提供了一種對象/關聯映射工具來管理Java
應用中的關係數據。他的出現主要是爲了簡化現有的持久化開發工作和整合ORM
技術,結束現在Hibernate
,TopLink
,JDO
等ORM
框架各自爲營的局面。值得注意的是,JPA
是在充分吸收了現有Hibernate
,TopLink
,JDO
等ORM
框架的基礎上發展而來的,具有易於使用,伸縮性強等優點。
注意:JPA是一套規範,不是一套產品,那麼像Hibernate,TopLink,JDO他們是一套產品,如果說這些產品實現了這個JPA規範,那麼我們就可以叫他們爲JPA的實現產品。
Spring-data-jpa
Spring-data-jpa
是 Spring
基於 ORM
框架、JPA
規範的基礎上封裝的一套JPA應用框架,可使開發者用極簡的代碼即可實現對數據的訪問和操作。它提供了包括增刪改查等在內的常用功能,且易於擴展!學習並使用 Spring-data-jpa
可以極大提高開發效率!
spring data jpa讓我們解脫了DAO層的操作,基本上所有CRUD都可以依賴於它來實現
Spring-data-jpa使用
基本查詢
基本查詢分爲兩種:
- spring data默認已經實現
- 根據查詢的方法來自動解析成SQL
預先生成方法
Spring-data-jpa
默認預先生成了一些基本的CURD的方法,例如:增、刪、改等等
/**
* 繼承JpaRepository,實現與數據庫交互(JPA支持自動生成一些基本CURD SQL語句)
*/
public interface UserRepository extends JpaRepository<User,Integer> {
}
使用默認方法
@Autowired
private PersonRepository personRepository;
@Test
public void testBaseQuery(){
Person person = new Person();
personRepository.findAll();
// personRepository.findOne();
personRepository.save(person);
personRepository.delete(person);
personRepository.count();
// personRepository.exists();
// ...
}
通過方法名可以看出每個具體的用途
自定義簡單查詢
自定義的簡單查詢就是根據方法名來自動生成SQL,主要的語法是findXXBy
,readAXXBy
,queryXXBy
,countXXBy
, getXXBy
後面跟屬性名稱:
User findByUserName(String userName);
也使用一些加一些關鍵字And、 Or
User findByUserNameOrSex(String username, String sex);
修改、刪除、統計也是類似語法
Long deleteById(Long id);
Long countByUserName(String userName)
基本上SQL體系中的關鍵詞都可以使用,例如:LIKE
、 IgnoreCase
、 OrderBy
。
List<User> findBySexLike(String sex);
User findByUserNameIgnoreCase(String userName);
List<User> findByUserNameOrderBySexDesc(String sex);
其中,相關命名規範如下:
關鍵字 | 方法命名 | JPQL snippet |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is,Equals | findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age ⇐ ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull | findByAgeIsNull | … where x.age is null |
IsNotNull,NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection ages) | … where x.age in ?1 |
NotIn | findByAgeNotIn(Collection age) | … where x.age not in ?1 |
TRUE | findByActiveTrue() | … where x.active = true |
FALSE | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstame) = UPPER(?1) |
PS:Spring-data-jpa
的能力遠不止本文提到的這些,由於本文主要以介紹接口測試開發爲主,對於Spring-data-jpa
的使用只是介紹了常見的使用方式。諸如 @Modifying
操作、分頁排序、原生SQL支持以及與Spring MVC
的結合使用等等內容就不在本文中詳細展開。
多數據源的支持
同源數據庫的多源支持
日常接口測試中因爲測試項目使用的分佈式開發模式,不同的服務有不同的數據源,常常需要在一個項目中使用多個數據源,因此需要配置Spring-data-jpa
對多數據源的使用,一般分一下爲三步:
- 配置多數據源
- 不同源的實體類放入不同包路徑
- 聲明不同的包路徑下使用不同的數據源、事務支持
異構數據庫多源支持
比如項目中,即需要對mysql
的支持,也需要對mongodb的查詢等。
實體類聲明 @Entity
關係型數據庫支持類型、聲明 @Document
爲mongodb
支持類型,不同的數據源使用不同的實體就可以了
interface PersonRepository extends Repository<Person, Long> {
…
}
@Entity
public class Person {
…
}
interface UserRepository extends Repository<User, Long> {
…
}
@Document
public class User {
…
}
但是,如果User用戶既使用 mysql 也使用 mongodb 呢,也可以做混合使用
interface JpaPersonRepository extends Repository<Person, Long> {
…
}
interface MongoDBPersonRepository extends Repository<Person, Long> {
…
}
@Entity
@Document
public class Person {
…
}
也可以通過對不同的包路徑進行聲明,比如 A 包路徑下使用mysql
,B 包路徑下使用mongoDB
@EnableJpaRepositories(basePackages = "com.zuozewei.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.zuozewei.repositories.mongo")
interface Configuration { }
使用示例
工程配置
在pom.xml
中添加相關依賴,加入以下內容:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
在application.yml中配置:
- 數據庫連接信息(如使用嵌入式數據庫則不需要)
- 自動創建表結構的設置
例如使用mysql的情況如下:
spring:
profiles:
active: a
datasource:
driver-class-name: com.mysql.jdbc.Driver
# 手動創建db_person數據庫
url: jdbc:mysql://39.105.21.22:3306/db_person?useUnicode=true&characterEncoding=utf-8&useSSL=false
username: zuozewei
password: zuozewei
jpa:
hibernate:
ddl-auto: update
# 顯示sql 但不會顯示具體的操作值 可以替換成log4jdbc
show-sql: true
server:
port: 8888
servlet:
context-path: /springboot
spring.jpa.properties.hibernate.hbm2ddl.auto
是hibernate
的配置屬性,其主要作用是:自動創建、更新、驗證數據庫表結構。該參數的幾種配置如下:
- create:每次加載 hibernate 時都會刪除上一次的生成的表,然後根據你的model類再重新來生成新表,哪怕兩次沒有任何改變也要這樣執行,這就是導致數據庫表數據丟失的一個重要原因。
create-drop
:每次加載hibernate
時根據Entity
類生成表,但是sessionFactory一關閉,表就自動刪除。update
:最常用的屬性,第一次加載hibernate
時根據Entity
類會自動建立起表的結構(前提是先建立好數據庫),以後加載hibernate
時根據model
類自動更新表結構,即使表結構改變了但表中的行仍然存在不會刪除以前的行。要注意的是當部署到服務器後,表結構是不會被馬上建立起來的,是要等應用第一次運行起來後纔會。validate
:每次加載hibernate
時,驗證創建數據庫表結構,只會和數據庫中的表進行比較,不會創建新表,但是會插入新值。
至此已經完成基礎配置,如果在Spring
下整合使用過它的話,相信你已經感受到Spring Boot
的便利之處:JPA的傳統配置在persistence.xml
文件中,但是這裏我們不需要。當然,最好在構建項目時候按照之前提過的最佳實踐的工程結構來組織,這樣以確保各種配置都能被框架掃描到。
創建實體
創建一個User
實體,包含id(主鍵)、name(姓名)、age(年齡)屬性,通過ORM
框架其會被映射到數據庫表中,由於配置了hibernate.hbm2ddl.auto
,在應用啓動的時候框架會自動去數據庫中創建對應的表。此處使用lombok效率插件,不熟悉的同學參照走進Java接口測試之效率插件lombok
/**
* 用戶實體類
* @author zuozwei
*
*/
@Entity
@Data
@Table(name = "person")
public class User {
//主鍵自增長
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private Integer age;
public User() {
}
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
}
創建數據訪問接口
下面針對User實體創建對應的Repository
接口實現對該實體的數據訪問
/**
* 數據訪問類
* @author zuozewei
*
*/
public interface UserRepository extends JpaRepository<User,Long> {
User findByName(String name);
User findByNameAndAge(String name, Integer age);
@Query("from User u where u.name=:name")
User findUser(@Param("name") String name);
}
在Spring-data-jpa
中,只需要編寫類似上面這樣的接口就可實現數據訪問。不再像我們以往編寫了接口時候還需要自己編寫接口實現類,直接減少了我們的文件清單。
在上例中,我們可以看到下面兩個函數:
User findByName(String name)
User findByNameAndAge(String name, Integer age)
它們分別實現了按 name
查詢 User
實體和按 name
和 age
查詢 User
實體,可以看到我們這裏沒有任何類SQL語句就完成了兩個條件查詢方法。這就是Spring-data-jpa
的一大特性:通過解析方法名創建查詢。
除了通過解析方法名來創建查詢外,它也提供通過使用 @Query
註解來創建查詢,只需要編寫JPQL
語句,並通過類似“:name
”來映射@Param
指定的參數,就像例子中的第三個findUser
函數一樣。
Junit單元測試
在完成了上面的數據訪問接口之後,按照慣例就是編寫對應的Junit單元測試來驗證編寫的內容是否正確。這裏就不多做介紹,主要通過數據操作和查詢來反覆驗證操作的正確性。
@RunWith(SpringRunner.class)
@SpringBootTest
public class userTest {
@Autowired
private UserRepository userRepository;
@Test
public void test(){
// 創建10條記錄
userRepository.save(new User("AAA", 10));
userRepository.save(new User("BBB", 20));
userRepository.save(new User("CCC", 30));
userRepository.save(new User("DDD", 40));
userRepository.save(new User("EEE", 50));
userRepository.save(new User("FFF", 60));
userRepository.save(new User("GGG", 70));
userRepository.save(new User("HHH", 80));
userRepository.save(new User("III", 90));
userRepository.save(new User("JJJ", 100));
// 測試findAll, 查詢所有記錄
Assert.assertEquals(10, userRepository.findAll().size());
// 測試findByName, 查詢姓名爲FFF的User
Assert.assertEquals(60, userRepository.findByName("FFF").getAge().longValue());
// 測試findUser, 查詢姓名爲FFF的User
Assert.assertEquals(60, userRepository.findUser("FFF").getAge().longValue());
// 測試findByNameAndAge, 查詢姓名爲FFF並且年齡爲60的User
Assert.assertEquals("FFF", userRepository.findByNameAndAge("FFF", 60).getName());
// 測試刪除姓名爲AAA的User
userRepository.delete(userRepository.findByName("AAA"));
// 測試findAll, 查詢所有記錄, 驗證上面的刪除是否成功
Assert.assertEquals(9, userRepository.findAll().size());
}
}
測試結果: