走進Java接口測試之持久層框架Spring-data-jpa

引言

在接口測試中把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技術,結束現在HibernateTopLinkJDOORM框架各自爲營的局面。值得注意的是,JPA是在充分吸收了現有HibernateTopLinkJDOORM框架的基礎上發展而來的,具有易於使用,伸縮性強等優點。

注意:JPA是一套規範,不是一套產品,那麼像Hibernate,TopLink,JDO他們是一套產品,如果說這些產品實現了這個JPA規範,那麼我們就可以叫他們爲JPA的實現產品。

Spring-data-jpa

Spring-data-jpaSpring 基於 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體系中的關鍵詞都可以使用,例如:LIKEIgnoreCaseOrderBy

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 關係型數據庫支持類型、聲明 @Documentmongodb支持類型,不同的數據源使用不同的實體就可以了

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.autohibernate的配置屬性,其主要作用是:自動創建、更新、驗證數據庫表結構。該參數的幾種配置如下:

  • 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 實體和按 nameage 查詢 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());

	}
}

測試結果:
在這裏插入圖片描述
在這裏插入圖片描述

工程結構

在這裏插入圖片描述

本文源碼:
https://github.com/zuozewei/Java-API-Test-Examples

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章