SpringBoot学习(五)操作数据库Spring-Data-JPA

时间:2022-09-11 12:41:37

一、JPA

在介绍Spring Data JPA的时候,我们首先认识下Hibernate。Hibernate是数据访问解决技术的绝对II主,使用0/R映射(Object-Relational Mapping)技术实现数据访问,O/R映射即将领域模型类和数据库的表进行映射,通过程序操作对象而实现表数据操作的能力,让数据访问操作无须关注数据库相关的技术。

随着Hibernate的盛行,Hibernate主导了EJB3.0的JPA规范,JPA即Java Persistence API。JPA是一个基于0/R映射的标准规范(目前最新版本是JPA2.1)。所谓规范即只定义标准规则(如注解、接口),不提供实现,软件提供商可以按照标准规范来实现,而使用者只需按照规范中定义的方式来使用,而不用和软件提供商的实现打交道。JPA的主要实现由Hibernate、Eclipse Link和OpenJPA等,这也意味着我们只要使用JPA来开发,无论是哪一个开发方式都是一样的。

Spring Data JPA是Spring Data的一个子项目,它通过提供基于JPA的Repository极大地减少了JPA作为数据访问方案的代码量。

在JPA中主要是使用函数名来分别数据库操作的,比如findByUsername,就是通过username查找记录,再比如findByUsernameAndPassword就是通过用户名密码访问数据,所以在JPA 中,函数的取名还是比较重要的,这部分也可通过代码提示来查看,因为他会根据数据库表字段和一些关键字推荐可用的部分函数名。当然JPA中也有一些原先就定义好的方法,比如findAll,save等方法。

在JPA定义函数名需要遵循下列规定:

Keyword Sample SQL
And findByLastnameAndFirstname where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstname
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 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith where x.firstname like ?1 (parameter bound with prepended %)
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(

二、配置

pom.xml文件中添加jpa依赖的包,很数据库连接库,这里使用的是mysql数据库。

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

配置连接信息,在application.yml文件中添加数据库连接的username,password等信息。这部分信息和之前在SpringMVC中的信息几乎是相同的。

spring:
datasource:
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
jpa:
database: MYSQL
hibernate:
ddl-auto: update
show-sql: true

SpringBoot中默认的连接池好像是org.apache.tomcat.jdbc.pool.DataSource。
在yml文件的代码提示中支持的连接池好像还有有dbcp,dbcp2,tomcat,hikari,但是按照提示之后好像在控制台看不到dbcp,dbcp2,tomcat,hikari这几个关键词,所以也不知道配置是否成功。

这里我还自己配置了阿里云的Druid连接池,就目前查看资料,感觉比较好的连接池有Druid和hikari,但是在实际中是哪几个比较好还不好说,下面是Druid的配置。

pom.xml文件中添加所需要的依赖库:

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.2</version>
</dependency>

然后在application.yml配置下面的信息

spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
jpa:
database: MYSQL
hibernate:
ddl-auto: update
show-sql: true

在springboot中配置这部分的信息相对而言还是比较简洁的

三、数据库映射类

package com.example.demo_2.JPA;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
//用下面这个注解也是可以的
//import org.hibernate.annotations.NamedQuery;

@Entity
@NamedQuery(name="Test.hhh",query="select t from Test t where t.username=? and t.password=?")
public class Test {
private int id;
private String username;
private String password;

public Test() {
}

public Test(int id, String username, String password) {
this.id = id;
this.username = username;
this.password = password;
}

public Test(String username, String password) {
super();
this.username = username;
this.password = password;
}

@Id
@GeneratedValue
public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

这个映射类还是很简单的,主要是上面有一个@NamedQuery注解,使用这个注解是可以自定义SQL语句来对数据库进行操作的。然后根据name(这里的name是hhh,不是Test.hhh)来调用这部分信息。

四、Repository类

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface TestRepository extends JpaRepository<Test, Integer> {

/**
* select * from Test t where t.username = ?
*
* @param username
* @return
*/

public List<Test> findByUsername(String username);

/**
* select * from Test t where t.username = ? and t.password = ?
*
* @param username
* @param password
* @return
*/

public List<Test> findByUsernameAndPassword(String username, String password);

/**
* 自定义query
*
* @return
*/

@Query("select t from Test t where t.id=:id")
public List<Test> findTestById(@Param("id") int id);

/**
* 与Test的NamedQuery相对应
*
* @param username
* @param password
* @return
*/

public List<Test> hhh(String username, String password);
}

这里面就是实现与操作数据库的操作。继承的JpaRepository中的前一个为数据库表对应的实体类,第二个为该表的主键的类型。看到最后有何叫hhh的函数,这个就是之前在Test类中自定义的SQL查询语句。然后在Jpa中自定义还可以在本类中是实现,就是findTestById,在函数名之前用@Query自定义SQL语句。

五、数据库的增删改查

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.web.bind.annotation.*;

import java.util.List;


@RestController
public class TestController {
@Autowired
private TestRepository testRepository;

/**
* 通过username获取Test数据
*
* @param username
* @return
*/

@GetMapping(value = "/testController/custom/{username}")
public List<Test> getbyUsername(@PathVariable("username") String username) {
return testRepository.findByUsername(username);
}

/**
* 测试通过函数名构建查询
*
* @param username
* @param password
* @return
*/

@RequestMapping("/testController/custom_1")
public List<Test> getByUsernameAndPassword(@RequestParam("username") String username,
@RequestParam("password") String password) {
return testRepository.findByUsernameAndPassword(username, password);
}

/**
* 通过Repository自定义SQL语句
*
* @param id
* @return
*/

@RequestMapping("/testController/custom_2")
public List<Test> getTestById(@RequestParam("id") int id) {
return testRepository.findTestById(id);
}

/**
* 通过Test类中的NamedQuery自定义查询语句
*
* @param username
* @param password
* @return
*/

@RequestMapping("/testController/custom_3")
public List<Test> hhh(@RequestParam("username") String username, @RequestParam("password") String password) {
return testRepository.hhh(username, password);
}

/**
* 排序
*
* @return
*/

@GetMapping(value = "/testController/custom/sort")
public List<Test> getbyUsernameSort() {
return testRepository.findAll(new Sort(Direction.ASC, "id"));
}

/**
* 分页+排序
*
* @return
*/

@GetMapping(value = "/testController/custom/pageable")
public Page<Test> getbyUsernamePage() {
// return testRepository.findAll(new PageRequest(1,2));
return testRepository.findAll(new PageRequest(1, 2, new Sort(Direction.ASC, "id")));
}

/**
* 获取所有数据
*
* @return
*/

@GetMapping(value = "/testController")
public List<Test> getAll() {
return testRepository.findAll();
}

/**
* Post测试,添加记录
* @param username
* @param password
* @return
*/

@PostMapping(value = "/testController")
public Test getAll(@RequestParam("username") String username, @RequestParam("password") String password) {
Test test = new Test();
test.setPassword(password);
test.setUsername(username);
return testRepository.save(test);
}

/**
* 测试自带的函数
* @param id
* @return
*/

@GetMapping(value = "/testController/{id}")
public Test getOne(@PathVariable("id") int id) {
return testRepository.findOne(id);
}

/**
* 测试删除数据
* @param id
*/

@DeleteMapping(value = "/testController/{id}")
public void deleteOne(@PathVariable("id") int id) {
testRepository.delete(id);
}
}

在这个类中主要是调用TestRepository中的方法。然后启动SpringBoot服务,分别访问对应的URL就可以得到对应的数据。在本类中,还有两个部分,分别是排序和分页这部分的内容,在JPA中也得到了很好地支持,这部分东西看代码还是容易看的,就不做解释了。

六、事务管理Transactional

Transactional中的属性主要是有下面几个。

属性 含义
Propagation
(默认REQUIRED)
Propagation定义了事务的生命周期主要有以下选项:
REQUIRED:方法A调用时没有事务新建一个事务,当在方法A调用另外一个方法B的时候,方法B将使用相同的事务;如果方法B发生异常需要数据回滚的时候,整个事务数据回滚
REQUIRES_NEW:对于方法A和B,在方法调用的时候无论是,否有事务都开启一个新的事务;这样如果方法B有异常不会导致,方法A的数据回滚
NESTED:和REQUIRES_NEW类似,但支持JDBC,不支持JPA或Hibernate
SUPPORTS:方法调用时有事务就用事务,没事务就不用事务
NOT_SUPPORTED:强制方法不在事务中执行,若有事务,在方法调用到结束阶段事务都将会被挂起
NEVER:强制方法不在事务中执行,若有事务则抛出异常
MANDATORY:强制方法在事务中执行,若无事务则抛出异常
Isolation
(默认DEFAULT)
Isolation(隔离)决定了事务的完整性,处理在多事务对相同数据下的处理机制,主要包含下面的隔离级别(当然我们也不可以随意设置,这要看当前数据库是否支持)
READ_UNCOMMITTED:对于在A事务里修改了一条记录但没有提交事务,在B事务可以读取到修改后的记录。可导致脏读、不可重复读以及幻读
READ_COMMITTED:只有当在A事务里修改了一条记录且提交事务之后,B事务才可以读取到提交后的记录;阻止脏读,但可能导致不可重复读和幻读
REPEATABLE_READ:不仅能实现 READ_COMMITTED 的功能,而且还能阻止到A事务读取了一条记录,B事务将不允许修改这条记录;阻止脏读和不可重复读,但可出现幻读
SERIALIZABLE:此级別下亊务足顺序执行的,可以避免上述级别的缺陷,似开销较大
DEFAULT:使用当前数据库的默认隔离界级别,如Oracle,SQL Server 是 READ_COMMITTED; Mysql是REPEATABLE_READ
timeout timeout指定事务过期时间,默认为当前数据库的事务过期时间
readOnly 指定当前事务是否只读事务,默认false
rollbackFor 指定哪个或者哪些异常可以引起事务回滚
noRollbackFor 指定哪个或者哪些异常不可以引起事务回滚

但是在这里很多属性是不方便测试的,这里就不做测试了。
在这里需要注意的是使用的是org.springframework.transaction.annotation.Transactional;包中的Transactional注解,若是使用javax.transaction.Transactional;的注解,该包中只有rollbackOn和dontRollbackOn这儿两个属性,其他的属性是没有的,当然使用这个包中的这两个属性也是完成可以的,但是其他的属性就没有办法测试了。
下面是测试代码(是在上面的那个部分完成之后才能做这个的)

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TranscationalController {

@Autowired
private TestRepository testRepository;

/**
* rollbackFor 抛出异常之后回滚数据,即新纪录不会被写进数据库
*
* @Transactional 使用的是org.springframework.transaction.annotation.Transactional;
* 不是 javax.transaction.Transactional;
*/

@RequestMapping("/transaction_1")
@Transactional(rollbackFor = { IllegalArgumentException.class })
public void transaction_1() {
testRepository.save(new Test("transaction_1", "transaction_1"));
throw new IllegalArgumentException("\rollbackFor");
}

/**
* noRollbackFor 抛出异常之后,不会滚数据,即新纪录还是会被写进数据库
*/

@RequestMapping("/transaction_2")
@Transactional(noRollbackFor = { IllegalArgumentException.class })
public void transaction_2() {
testRepository.save(new Test("transaction_2", "transaction_2"));
throw new IllegalArgumentException("\noRollbackFor");
}

/**
* readOnly = true
* 会出现异常:Queries leading to data modification are not allowed
*/

@RequestMapping("/transaction_3")
@Transactional(readOnly = true)
public Test transaction_3() {
return testRepository.save(new Test("transaction_3", "transaction_3"));
}

/**
* 其他的与transaction相关的还有isolation,timeout和propagation,这三个不好测试,感觉用默认的就挺好用的了
*/


}

七、缓存

注解 解释
@Cacheable 若缓存中已经有数据,则直接读取数据,若无,则将内容存进缓存
@CachePut 不管缓存中是否存在该值,都会将把内容存进缓存
@CacheEvict 删除缓存
@Caching 可以通过该注解组合多个注解策略在一个方法上
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
* 需要在Demo2Application.java中添加@EnableCaching注解
* @Caching 可以通过该注解组合多个注解策略在一个方法上
* JPA中注解几乎都是通过AOP的方式使用的
*/

@RestController
public class CacheController {

@Autowired
private TestRepository testRepository;

/**
* 不管怎么样都会将把内容存进缓存
*
* @param id
* @return
*/

@RequestMapping("/cache_1")
@CachePut(value = "test", key = "#id")
public Test CachePut(@RequestParam("id") int id) {
return testRepository.findOne(id);
}

/**
* 若缓存中已经有数据,则直接读取数据,若无,则将内容存进缓存
*
* @param id
* @return
*/

@RequestMapping("/cache_2")
@Cacheable(value = "test", key = "#id")
public Test Cacheable(@RequestParam("id") int id) {
return testRepository.findOne(id);
}

/**
* 删除缓存
*
* @param id
* @return
*/

@RequestMapping("/cache_3")
@CacheEvict(value = "test")
public Test CacheEvict(@RequestParam("id") int id) {
return testRepository.findOne(id);
}
}

测试缓存的方法是,若缓存中存在该记录,则重新访问一个URL获取同一个数据,则在后台的console中是不会输出SQL语句的,就是不会重新从数据库中获取数据,二是从缓存中直接拿去数据。