第七章 使用JPA操作数据库
本章主要介绍如何在Spring Boot的Web应用中使用Mysq数据库,也充分展示Spring Boot的优势(尽可能少的代码和配置).
数据访问层我们将使用Spring Data JPA和Hibernate(JPA的实现之一).
Maven pom.xml文件
lightsword/pom.xml
在项目中增加如下依赖文件
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
配置文件application.properties
在src/main/resources/application.properties中设置数据源和jpa配置:
#mysql
spring.datasource.url = jdbc:mysql://localhost:3306/lightsword?useUnicode=true&characterEncoding=UTF8
spring.datasource.username = root
#root@localhost ::TZaMojg3ntd
spring.datasource.password = root
spring.datasource.driverClassName = com.mysql.jdbc.Driver
spring.datasource.max-active=0
spring.datasource.max-idle=0
spring.datasource.min-idle=0
spring.datasource.max-wait=10000
spring.datasource.max-wait-millis=31536000
# Specify the DBMS
spring.jpa.database = MYSQL
# Show or not log for each sql query
spring.jpa.show-sql = true
# Hibernate ddl auto (create, create-drop, update)
spring.jpa.hibernate.ddl-auto = update
# Naming strategy
spring.jpa.hibernate.naming-strategy = org.hibernate.cfg.ImprovedNamingStrategy
# stripped before adding them to the entity manager)
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
全部的配置都在如上的文件中了,不需要另外的XML配置和Java配置。
上文中的数据库配置,你需要换成你的数据库的地址和用户名密码。
hibernate的ddl-auto=update配置表名,数据库的表和列会自动创建(根据Java实体类,在scala中,只要在实体类上标注@Entity,成员变量上标注@BeanProperty),这里 可以看到更多得hibernate配置。
实体类
创建一个HttpApi实体类,实体和Mysql数据库的http_api表相对应(这个表字段会在应用启动的时候,自动生成)。
package com.springboot.in.action.entity
import java.util.Date
import javax.persistence.{ Entity, GeneratedValue, GenerationType, Id }
import scala.language.implicitConversions
import scala.beans.BeanProperty
@Entity
class HttpApi {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@BeanProperty
var id: Integer = _
@BeanProperty
var httpSuiteId: Integer = _
//用例名称
@BeanProperty
var name: String = _
//用例状态: -1未执行 0失败 1成功
@BeanProperty
var state: Integer = _
//接口
@BeanProperty
var url: String = _
//方法GET,POST
@BeanProperty
var method: String = _
//post参数json string
@BeanProperty
var paramJsonStr: String = _
//期望输出
@BeanProperty
var expectOutput: String = _
//实际输出
@BeanProperty
var actualOutput: String = _
@BeanProperty
var runTimes: Integer = _
@BeanProperty
var owner: String = _
@BeanProperty
var gmtCreate: Date = _
@BeanProperty
var gmtModify: Date = _
}
实体的数据访问层HttpApiDao
实体的数据访问层HttpApiDao非常简单,只需要继承CrudRespositroy即可,CrudRespositroy已经实现了save,delete,deleteAll,findOne和findAll.
(比较神奇的时这些方法其实CrudRespositroy中其实并没有实现,并且通过对dao层的方法的命名还可以实现新的方法).
当然,如果基本的CRUD方法满足不了我们稍微复杂一些的sql查询,我们可以直接定义sql查询语句,绑定dao层的方法.实例在如下代码中可以看到:
package com.springboot.in.action.dao
import java.util.List
import com.springboot.in.action.entity.HttpApi
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository
import scala.language.implicitConversions
trait HttpApiDao extends CrudRepository[HttpApi, Integer] {
def findAll(): List[HttpApi] // JavaConversions
def save(t: HttpApi): HttpApi
def findOne(id: Integer): HttpApi
@Query(value = "SELECT * FROM http_api where http_suite_id = ?1", nativeQuery = true)
def listByHttpSuiteId(id: Integer): List[HttpApi]
@Query(value = "SELECT id FROM http_api where http_suite_id = ?1", nativeQuery = true)
def listTestCaseId(httpSuiteId: Integer): List[Integer] // 隐式转换,直接用scala的List会报错:javax.persistence.NonUniqueResultException: result returns more than one elements] with root cause
@Query(value = "SELECT * FROM http_api where name like %?1% ", nativeQuery = true) // like '%?%'
def findByName(name: String): List[HttpApi]
@Query(value = "select count(*) from http_api where http_suite_id = ?1 and state = 1", nativeQuery = true)
def countPass(httpSuiteId: Integer): Int
@Query(value = "select count(*) from http_api where http_suite_id = ?1 and state = 0", nativeQuery = true)
def countFail(httpSuiteId: Integer): Int
}
重点看一下
- @Query注解里面的value和nativeQuery=true,意思是使用原生的sql查询语句.
- sql模糊查询like语法,我们在写sql的时候是这样写的
like '%?%'
但是在@Query的value字符串中, 这样写
SELECT * FROM http_api where name like %?1%
- 在Springboot跟scala集成开发过程中,集合类需要使用java里面的api,直接用scala的List会报错:
javax.persistence.NonUniqueResultException: result returns more than one elements] with root cause.
可以显示声明:
import java.util.List
也可以使用隐式转换:
import scala.collection.JavaConversions._
控制器HttpApiController
新建控制器HttpApiController.scala代码
package com.springboot.in.action.controller
import java.util.Date
import java.util.concurrent.CountDownLatch
import com.alibaba.fastjson.JSON
import com.springboot.in.action.dao.{HttpApiDao, HttpReportDao, HttpSuiteDao}
import com.springboot.in.action.engine.OkHttp
import com.springboot.in.action.entity.{HttpApi, HttpReport}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.{PathVariable, RequestMapping, RequestMethod, RequestParam, ResponseBody, RestController}
import org.springframework.web.servlet.ModelAndView
import scala.collection.JavaConversions._
@RestController
@RequestMapping(Array("/httpapi"))
class HttpApiController @Autowired() (
val HttpSuiteDao: HttpSuiteDao,
val HttpApiDao: HttpApiDao,
val HttpReportDao: HttpReportDao) {
@RequestMapping(value = {
Array("", "/")
}, method = Array(RequestMethod.GET))
def list(model: Model) = {
model.addAttribute("httpapis", HttpApiDao.findAll())
new ModelAndView("/httpapi/list")
}
@RequestMapping(value = {
Array("/json")
}, method = Array(RequestMethod.GET))
def listJson() = HttpApiDao.findAll()
@RequestMapping(value = {
Array("/listHttpSuiteTestCase")
}, method = Array(RequestMethod.GET))
def listHttpSuiteTestCase(model: Model, @RequestParam(value = "httpSuiteId") httpSuiteId: Integer) = {
var httpapis = HttpApiDao.listByHttpSuiteId(httpSuiteId)
model.addAttribute("httpapis", httpapis)
model.addAttribute("httpSuiteId", httpSuiteId)
model.addAttribute("httpSuiteName", HttpSuiteDao.findOne(httpSuiteId).name)
new ModelAndView("/httpapi/listHttpSuiteTestCase")
}
@RequestMapping(value = {
Array("/listHttpSuiteTestCaseJson")
},
method = Array(RequestMethod.GET))
@ResponseBody
def listHttpSuiteTestCaseJson(model: Model, @RequestParam(value = "httpSuiteId") httpSuiteId: Integer) = {
HttpApiDao.listByHttpSuiteId(httpSuiteId)
}
@RequestMapping(Array("/newPage/{httpSuiteId}"))
def goNewPage(@PathVariable(value = "httpSuiteId") httpSuiteId: Integer, model: Model) = {
model.addAttribute("httpSuiteId", httpSuiteId)
model.addAttribute("httpSuiteName", HttpSuiteDao.findOne(httpSuiteId).name)
new ModelAndView("/httpapi/new")
}
/**
* 项目下面的用例编辑
*/
@RequestMapping(Array("/editPage/{caseId}"))
def goEditPage(model: Model, @PathVariable(value = "caseId") caseId: Integer, @RequestParam(value = "httpSuiteId") httpSuiteId: Integer) = {
val httpapi = HttpApiDao.findOne(caseId)
model.addAttribute("httpapi", httpapi)
model.addAttribute("httpSuiteId", httpSuiteId)
model.addAttribute("httpSuiteName", HttpSuiteDao.findOne(httpSuiteId).name)
new ModelAndView("/httpapi/edit")
}
@RequestMapping(Array("/copyPage/{caseId}"))
def goCopyPage(model: Model, @PathVariable(value = "caseId") caseId: Integer, @RequestParam(value = "httpSuiteId") httpSuiteId: Integer) = {
val httpapi = HttpApiDao.findOne(caseId)
model.addAttribute("httpapi", httpapi)
model.addAttribute("httpSuiteId", httpSuiteId)
model.addAttribute("httpSuiteName", HttpSuiteDao.findOne(httpSuiteId).name)
new ModelAndView("/httpapi/copy")
}
@RequestMapping(Array("/detailPage/{id}"))
def goDetailPage(model: Model, @PathVariable(value = "id") id: Integer) = {
val httpapi = HttpApiDao.findOne(id)
model.addAttribute("httpapi", httpapi)
new ModelAndView("/httpapi/detail")
}
@RequestMapping(value = Array("/postnew"),
method = Array(RequestMethod.POST))
@ResponseBody
def newOne(@RequestParam(value = "httpSuiteId") httpSuiteId: Integer,
@RequestParam(value = "name") name: String,
@RequestParam(value = "url") url: String,
@RequestParam(value = "method") method: String,
@RequestParam(value = "paramJsonStr") paramJsonStr: String,
@RequestParam(value = "expectOutput") expectOutput: String,
@RequestParam(value = "actualOutput") actualOutput: String,
@RequestParam(value = "owner") owner: String) = {
val httpapi = new HttpApi()
httpapi.httpSuiteId = httpSuiteId
httpapi.name = name
httpapi.url = url
httpapi.method = method
httpapi.paramJsonStr = paramJsonStr
httpapi.expectOutput = expectOutput
httpapi.actualOutput = actualOutput
httpapi.runTimes = 0
httpapi.state = -1
httpapi.owner = owner
httpapi.gmtCreate = new Date()
httpapi.gmtModify = new Date()
HttpApiDao.save(httpapi)
}
@RequestMapping(value = Array("/postedit"),
method = Array(RequestMethod.POST))
@ResponseBody
def editOne(@RequestParam(value = "id") id: Integer,
@RequestParam(value = "name") name: String,
@RequestParam(value = "url") url: String,
@RequestParam(value = "method") method: String,
@RequestParam(value = "paramJsonStr") paramJsonStr: String,
@RequestParam(value = "expectOutput") expectOutput: String) = {
val httpapi = HttpApiDao.findOne(id)
httpapi.name = name
httpapi.url = url
httpapi.method = method
httpapi.paramJsonStr = paramJsonStr
httpapi.expectOutput = expectOutput
httpapi.gmtModify = new Date()
HttpApiDao.save(httpapi)
}
/**
* 在新建用例页面,调试用例用
*/
@RequestMapping(value = Array("/debugTest"),
method = Array(RequestMethod.GET))
@ResponseBody
def debugTest(@RequestParam(value = "url") url: String,
@RequestParam(value = "method") method: String,
@RequestParam(value = "paramJsonStr") paramJsonStr: String) = {
OkHttp.run(url, method, paramJsonStr)
}
/**
* 执行用例
*/
@RequestMapping(value = Array("/runTest"),
method = Array(RequestMethod.GET))
@ResponseBody
def debugTest(@RequestParam(value = "id") id: Integer) = { runTestCase(id) }
/**
* 回归项目全部用例,每个用例单独起一个线程跑
*/
@RequestMapping(value = Array("/testHttpSuite"),
method = Array(RequestMethod.GET))
@ResponseBody
def testProject(@RequestParam(value = "httpSuiteId") httpSuiteId: Integer) = {
val caseIds = HttpApiDao.listTestCaseId(httpSuiteId)
val threads = caseIds.size
val countDownLatch = new CountDownLatch(threads)
for (cid <- caseIds) {
val t = new TestCaseRunner(cid, countDownLatch)
t.start
}
println("回归测试开始......")
countDownLatch.await // now waiting sub thread done.
println("回归测试结束!")
val HttpReport = getHttpReport(httpSuiteId)
// 保存测试结果
HttpReportDao.save(HttpReport)
HttpReport
}
def getHttpReport(httpSuiteId: Integer) = {
println("自动化回归测试报告:")
val p = HttpSuiteDao.findOne(httpSuiteId)
val httpSuiteName = p.name
val pass = HttpApiDao.countPass(httpSuiteId)
val fail = HttpApiDao.countFail(httpSuiteId)
val HttpReport = new HttpReport
HttpReport.httpSuiteId = httpSuiteId
HttpReport.httpSuiteName = httpSuiteName
HttpReport.pass = pass
HttpReport.fail = fail
HttpReport.time = new Date
println(JSON.toJSONString(HttpReport, true))
HttpReport
}
/**
* 执行caseId这个用例
*/
def runTestCase(id: Integer) = {
val tc = HttpApiDao.findOne(id)
val url = tc.url
val method = tc.method
val paramJsonStr = tc.paramJsonStr
println("接口url:" + url)
println("方法:" + method)
println("输入参数:" + paramJsonStr)
val result = OkHttp.run(url, method, paramJsonStr)
//执行次数+1
tc.runTimes = tc.runTimes + 1
println("实际输出:" + result)
tc.actualOutput = result
// 结果断言
val expectOutput = tc.expectOutput
val contains = result.contains(expectOutput)
tc.state = if (contains) 1 else 0
// 执行事件
tc.gmtModify = new Date
HttpApiDao.save(tc)
}
/**
* TestCaseRunner
*/
class TestCaseRunner(val caseId: Integer, val countDownLatch: CountDownLatch) extends Thread {
override def run() {
runTestCase(caseId)
countDownLatch.countDown
}
}
}
自动装配@Autowired Dao层代码,在Controller实现业务逻辑.
运行测试
启动脚本lightsword/run.sh
mvn clean scala:compile scala:run -Dlauncher=app
运行lightsword/run.sh,启动应用.
新建用例集,然后在此用例集中新建一个测试用例,如图所示:
可以直接运行,可以看到测试结果.
也可以浏览器访问:
http://localhost:8888/httpapi/listHttpSuiteTestCaseJson?httpSuiteId=1
看到Restful接口的json返回:
[{"id":1,"httpSuiteId":1,"name":"HelloSB测试","state":1,"url":"http://localhost:8888/hello","method":"GET","paramJsonStr":"{}","expectOutput":"LightSword","actualOutput":"{\"conent\":\"Hello, LightSword! Now is: Mon Jun 27 13:23:20 CST 2016\"}","runTimes":1,"owner":"陈光剑","gmtCreate":1467004998000,"gmtModify":1467005001000}]