Spring Data项目是Spring用来解决数据访问问题的一揽子解决方案。
全部章节传送门:
Spring Boot学习笔记(一):Spring Boot 入门基础
Spring Boot学习笔记(二):Spring Boot 运行原理
Spring Boot学习笔记(三):Spring Boot Web开发
Spring Boot学习笔记(四):Spring Boot 数据访问
Spring Boot学习笔记(五):Spring Boot 企业级开发
Spring Boot学习笔记(六):Spring Boot 应用监控
Spring Data JPA
Spring Data JPA是Spring Data的一个子项目,它提供了一套简化JPA开发的框架,用来简化数据库访问。同时提供了很多除了CRUD之外的功能,如分页、排序、复杂查询等等。
准备环境
创建数据表
在MySQL数据库中建立一个数据表t_person,用来进行后面的测试,并在里面随便添加几条数据。
create table t_person (
id INT PRIMARY KEY AUTO_INCREMENT,
name varchar(10),
age INT,
address VARCHAR(20)
) CHARACTER SET UTF8;
建立项目
建立一个springboot项目,在pom.xml中添加如下依赖。其中guava是一个工具包。
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-jdbc
com.google.guava
guava
18.0
org.springframework.boot
spring-boot-starter-test
test
配置基本属性
在application.properties里面配置数据源和JPA相关属性。
# 数据库相关
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springstudy
spring.datasource.username=spring
spring.datasource.password=spring
spring.datasource.dirverClassName=com.mysql.jdbc.Driver
# 根据实体类自动维护数据库表结构的功能
spring.jpa.hibernate.ddl-auto=none
# 设置hibernate操作的时候在控制台显示真实的SQL语句
spring.jpa.show-sql=true
# 让控制器输出的json字符串格式更美观
spring.jackson.serialization.indent_output=true
其中,spring.jpa.hibernate.ddl-auto 提供根据实体类自动维护数据库表结构的功能,可选值包括:
- create----每次运行该程序,没有表格会新建表格,表内有数据会清空。
- create-drop----每次程序结束的时候会清空表。
- update----每次运行程序,没有表格会新建表格,表内有数据不会清空,只会更新。
- validate----运行程序会校验数据与数据库的字段类型是否相同,不同会报错。
- none----不采取任何措施。
定义映射实体类
创建实体类Person,将数据表的字段映射过来。
其中,@Entity注解表明这个类是一个实体,任何Hibernate映射对象都要有这个注解,@Table注解用来映射表名,@Column注解用来映射属性名和字段名,不注解的时候可以自动映射,比如将name映射为NAME,将testName映射为TEST_NAME。
package com.wyk.datademo;
import javax.persistence.*;
@Entity
@Table(name = "t_person") //表名
public class Person {
@Id //映射为数据库主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // 生成方式为自增
private Long id;
private String name;
private Integer age;
private String address;
public Person() {}
public Person(Long id, String name, Integer age, String address) {
this.id = id;
this.name = name;
this.age = age;
this.address = address;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
定义数据访问层
使用Spring Data JPA建立数据访问层需要定义一个继承JapRepository的接口。
package com.wyk.datademo;
import org.springframework.data.jpa.repository.JpaRepository;
public class PersonRespositary extends JpaRepository {
...
}
JpaRepository接口存在如下数据访问操作方法。
package org.springframework.data.jpa.repository;
import java.util.List;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.QueryByExampleExecutor;
@NoRepositoryBean
public interface JpaRepository extends PagingAndSortingRepository, QueryByExampleExecutor {
List findAll();
List findAll(Sort var1);
List findAllById(Iterable var1);
List saveAll(Iterable var1);
void flush();
S saveAndFlush(S var1);
void deleteInBatch(Iterable var1);
void deleteAllInBatch();
T getOne(ID var1);
List findAll(Example var1);
List findAll(Example var1, Sort var2);
}
配置使用 Spring Data JPA
在Spring中,可以通过@EnableJpaRepositories注解来开启Spring Data JPA的支持,通过接收的value参数来扫描数据访问层所在包下的数据访问接口定义。
package com.wyk.datademo;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import javax.persistence.EntityManagerFactory;
@Configuration
@EnableJpaRepositories("com.wyk.datademo")
public class JpaConfigurattion {
public EntityManagerFactory entityManagerFactory() {
...
}
...
}
在Sring Boot中,会进行自动配置,不需要添加如上配置代码。
定义查询方法
根据属性名查询
Spring Data JPA支持通过定义在Repository接口中的方法名来定义查询,而方法名是根据实体类的属性名来确定。
根据属性名来定义查询方法。
public interface PersonRepository extends JpaRepository {
/**
* 通过名字查询
* 相当于 select p from Person p where p.name=?1
* @param name
* @return
*/
List findByName(String name);
/**
* 通过名字模糊查询
* 相当于select p from Person p where p.name like ?1
* @param name
* @return
*/
List findByNameLike(String name);
/**
* 通过名字和地址查询
* 相当于 select p from Perosn p where p.name=?1 and p.address=?2
* @param name
* @param address
* @return
*/
List findByNameAndAddress(String name, String address);
}
还可以通过top和first等关键字来限制结果数量。
```java
/**
* 查询符合条件的前10条数据
* @param name
* @return
*/
List findFirst10ByName(String name);
/**
* 查询符合条件的奇拿30条数据
* @param name
* @return
*/
List findTop30ByName(String name);
使用JPA的NamedQuery查询
Spring Data JPA 支持用JPA的NamedQuery来定义查询方法,即一个名称映射一个查询语句。需要在实体类上添加@NamedQuery注解。
@Entity
@NamedQuery(name="Person.withNameAndAddressNamedQuery",
query = "select p from Person p where p.name = ?1 and address=?2")
@Table(name = "t_person") //表名
public class Person {
...
}
查询使用如下语句。
/**
* 使用NamdeQuery里定义的查询语句
* @param name
* @return
*/
Person withNameAndAddressQuery(String name, String address);
使用@Query查询
Spring Data JPA 还支持用@Query注解在接口的方法上实现查询,可以根据参数索引。
@Query("select p from Person p where p.name=?1 and p.address=?2")
Person withNameAndAddressQuery(String name, String address);
还可以使用参数的名称来匹配查询参数。
@Query("select p from Person p where p.address= :address")
List findByAddress(@Param("address")String address);
Spring Data JPA 支持@Modifying和@Query注解组合来事件更新查询。
@Modifying
@Transactional
@Query("update Person p set p.name=?1")
int setName(String name);
分页与排序
Spring Data JPA 也对排序和分页提供了支持。
/**
* 查询结果排序
* @param name
* @param sort
* @return
*/
List findByName(String name, Sort sort);
/**
* 查询结果分页
* @param name
* @param pageable
* @return
*/
Page findByName(String name, Pageable pageable);
使用排序:
List people = personRepository.findByName("haha", new Sort(Sort.Direction.ASC, "age"));
使用分页:
Page people2 = personRepository.findByName("haha", PageRequest.of(0, 10));
Page接口可有获取当前页面记录、总页数、总记录数等。
Specification
JPA 提供了基于准则查询的方式,即Criteria查询,可以用来进行复杂的动态查询。Spring Data JPA 提供了一个Specification接口,其中定义了一个toPredicate方法用来构造查询条件。
定义一个Criterial查询。其中Root用来获取需要查询的属性,通过CriteriaBuilder构造查询条件(例子中是来自北京的人).
public class CustomerSpecs {
public static Specification personFromBeijing() {
return new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> criteriaQuery,
CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.get("address"), "北京");
}
};
}
}
在接口类上需要实现JpaSpecificationExecutor接口。
public interface PersonRepository extends JpaRepository,
JpaSpecificationExecutor {
...
}
使用的时候需要静态导入。
import static com.wyk.datademo.CustomerSpecs.*;
注入personRepository的Bean后可以调用方法。
List people = personRepository.findAll(perosnFromBeijing());
添加控制器
将PersonRepository注入到控制器。
package com.wyk.datademo;
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.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class DataController {
//Spring Data JPA 已自动为你注册bean,所以可自动注入
@Autowired
PersonRepository personRepository;
/**
* 保存
* @param name
* @param address
* @param age
* @return
*/
@RequestMapping("/save")
public Person save(String name, String address, Integer age) {
// save 支持批量保存
Person p = personRepository.save(new Person(null, name, age, address));
return p;
}
/**
* 测试findByAddress
* @param address
* @return
*/
@RequestMapping("/q1")
public List q1(String name) {
List people = personRepository.findByName(name);
return people;
}
/**
* 测试findByNameAndAddress
* @param name
* @param address
* @return
*/
@RequestMapping("/q2")
public Person q2(String name, String address) {
Person people = personRepository.findByNameAndAddress(name, address);
return people;
}
/**
* 测试withNameAndAddressQuery
* @param name
* @param address
* @return
*/
@RequestMapping("/q3")
public Person q3(String name, String address) {
Person people = personRepository.withNameAndAddressQuery(name, address);
return people;
}
/**
* 测试withNameAndAddressNamedQuery
* @param name
* @param address
* @return
*/
@RequestMapping("/q4")
public Person q4(String name, String address) {
Person people = personRepository.withNameAndAddressNamedQuery(name, address);
return people;
}
/**
* 测试排序
* @return
*/
@RequestMapping("/sort")
public List sort() {
List people = personRepository.findAll(new Sort(Sort.Direction.ASC, "age"));
return people;
}
/**
* 测试分页
* @return
*/
@RequestMapping("/page")
public Page page() {
Page pagePeople = personRepository.findAll(PageRequest.of(1, 2));
return pagePeople;
}
}
查看运行结果
运行项目,依次查看结果:
保存实体(http://localhost:8080/save?name=ss&address=Shanghai&age=25):
根据属性查询(http://localhost:8080/q1?name=xiaoming):
根据多条属性查询(http://localhost:8080/q2?name=xiaoming&address=Beijing):
根据@Query注解查询(http://localhost:8080/q3?name=xiaoming&address=Beijing):
根据NamedQuery查询(http://localhost:8080/q4?name=xiaoming&address=Beijing):
查询结果排序(http://localhost:8080/sort):
查询结果分页(http://localhost:8080/sort):
自定义Repository的实现
我们可以通过Spring Data JPA的JpaRepository封装自己的数据库操作,提供给Repository接口使用。
定义Specification
首先需要定义Specification,本部分定制一个自动模糊查询:当值为字符型时使用like查询,其余类型使用等于查询,没有值就查询全部。
package com.wyk.datademo;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.persistence.metamodel.Attribute;
import javax.persistence.metamodel.EntityType;
import javax.persistence.metamodel.SingularAttribute;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import static com.google.common.collect.Iterables.toArray;
public class CustomerSpecs {
/**
* 定义一个返回值为Specification的方法byAuto
* @param entityManager
* @param example
* @param
* @return
*/
public static Specification byAuto(final EntityManager entityManager,
final T example) {
// 获得当前实体类对象的类型
final Class type = (Class)example.getClass();
return new Specification() {
@Override
public Predicate toPredicate(Root root, CriteriaQuery> criteriaQuery,
CriteriaBuilder criteriaBuilder) {
// 新建Predicate列表存储构造的查询条件
List predicates = new ArrayList<>();
// 获取实体类的entityType,我们可以从中获得实体类的属性
EntityType entity = entityManager.getMetamodel().entity(type);
//对实体类的属性进行循环
for(Attribute attr : entity.getDeclaredAttributes()) {
// 获取实体类对象某一属性的值
Object attrValue = getValue(example, attr);
if(attrValue != null) {
// 当前属性为字符类型的时候
if(attr.getJavaType() == String.class) {
// 当前字符不为空的情况下
if(!StringUtils.isEmpty(attrValue)) {
// 构造当前属性like查询条件,并添加条件列表
predicates.add(criteriaBuilder.like(root.get(attribute(entity, attr.getName(),
String.class)), pattern((String) attrValue)));
} else {
// 构造属性和属性值equal查询条件,并添加到条件列表中
predicates.add(criteriaBuilder.equal(root.get(attribute(entity,
attr.getName(), attrValue.getClass())), attrValue));
}
}
}
}
//将条件列表转换成Predicate
return predicates.isEmpty() ? criteriaBuilder.conjunction() :
criteriaBuilder.and(toArray(predicates, Predicate.class));
}
/**
* 通过反射获取实体类对象对应属性的属性值
* @param example
* @param attr
* @param
* @return
*/
private Object getValue(T example, Attribute attr) {
return ReflectionUtils.getField((Field) attr.getJavaMember(), example);
}
/**
* 获取实体类当前属性的SingularAttribute
* @param entity
* @param fieldName
* @param fieldClass
* @param
* @param
* @return
*/
private SingularAttribute attribute(EntityType entity,
String fieldName, Class fieldClass) {
return entity.getDeclaredSingularAttribute(fieldName, fieldClass);
}
};
}
/**
* 构造like的查询模式
* @param str
* @return
*/
static private String pattern(String str) {
return "%" + str + "%";
}
}
定义Repository接口
定义一个继承JpaRepository的接口,使它具备JpaRepository接口的所有方法,还继承了JpaSpecificationExecutor,具备使用Specification的能力。
package com.wyk.datademo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.NoRepositoryBean;
import java.io.Serializable;
@NoRepositoryBean //表示当前接口不是领域类的接口
public interface CustomRepository extends
JpaRepository, JpaSpecificationExecutor {
Page findByAuto(T example, Pageable pageable);
}
定义接口实现
定义一个实现前面接口的类,并继承SimpleJpaRepository,让我们可以使用SimpleJpaRepository中的方法。
package com.wyk.datademo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import javax.persistence.EntityManager;
import java.io.Serializable;
import static com.wyk.datademo.CustomerSpecs.byAuto;
public class CustomRepositoryImpl extends
SimpleJpaRepository implements CustomRepository {
private final EntityManager entityManager;
public CustomRepositoryImpl(Class domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
this.entityManager = entityManager;
}
/**
* 实现用byAuto的条件查询,并提供分页查询
* @param example
* @param pageable
* @return
*/
@Override
public Page findByAuto(T example, Pageable pageable) {
return findAll(byAuto(entityManager, example), pageable);
}
}
定义repositoryFactoryBean
自定义repositoryFactoryBean扩展JpaRepositoryFactoryBean,可以从获得一个RepositoryFactory。
package com.wyk.datademo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactory;
import org.springframework.data.jpa.repository.support.JpaRepositoryFactoryBean;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.data.repository.core.RepositoryInformation;
import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
import javax.persistence.EntityManager;
import java.io.Serializable;
public class CustomRepositaryFactoryBean, S, ID
extends Serializable> extends JpaRepositoryFactoryBean {
public CustomRepositaryFactoryBean(Class extends T> repositoryInterface) {
super(repositoryInterface);
}
@Override
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
return new CustomRepositoryFactory(entityManager);
}
private static class CustomRepositoryFactory
extends JpaRepositoryFactory {
public CustomRepositoryFactory(EntityManager entityManager) {
super(entityManager);
}
@Override
@SuppressWarnings({"unchecked"})
protected SimpleJpaRepository, ?> getTargetRepository(
RepositoryInformation information, EntityManager entityManager) {// 获得当前自定义类的实现
return new CustomRepositoryImpl((Class) information.getDomainType(), entityManager);
}
/*
@Override
@SuppressWarnings({"unchecked"})
protected SimpleJpaRepository, ?>
getTargetRepository (RepositoryInformation information,
EntityManager entityManager) {
return new CustomRepositoryImpl((Class) information.getDomainType(),
entityManager);
}
*/
@Override
protected Class> getRepositoryBaseClass(RepositoryMetadata metadata) {
return CustomRepositoryImpl.class;
}
}
}
使用自定义仓库
让实体类的Repository继承自定义的Repository接口,即可使用自定义Repository中实现的功能。
public interface PersonRepository extends CustomRepository,
JpaSpecificationExecutor {
...
}
在控制器中添加测试方法。
/**
* 测试自定义仓库
* @param person
* @return
*/
@RequestMapping("/auto")
public Page auto(Person person) {
Page pagePeople = personRepository.findByAuto(person, PageRequest.of(0, 10));
return pagePeople;
}
在运行类上使用@EnableJpaRepositories让自定义的Repoisitory生效。
@SpringBootApplication
@EnableJpaRepositories(repositoryFactoryBeanClass = CustomRepositaryFactoryBean.class)
public class DatademoApplication {
public static void main(String[] args) {
SpringApplication.run(DatademoApplication.class, args);
}
}
查看效果
运行程序,访问http://localhost:8080/auto, 无查询条件,查看结果。
访问http://localhost:8080/auto?address=h,进行模糊查询。
Spring Data REST
Spring Data REST是基于Spring Data的repository之上,可以将repository自动输出为REST资源。
配置Spring Data REST
Spring Data REST的配置是定义在RepositoryRestMvcConfiguration配置类中,我们可以通过继承此类或者直接在自己的配置类上@Import此配置类。
继承方式:
@Configuration
public class MyRepositoryRestMvcConfiguration extends RepositoryRestMvcConfiguration {
@Override
public RepositoryRestConfiguraiton config() {
return super.config();
}
//其它可重写以config开头的方法
...
}
导入方式:
@Configuration
@Import(RepositoryRestMvcConfiguration.class)
public class AppConfig {
...
}
Spring Boot对Spring Data REST的自动配置放置在rest包中.通过SpringBootRestConfiguration类的源码我们可以得出,Spring Boot已经为我们自动配置了RepositoryRestConfiguration,所以在Spring boot中使用Spring Data REST只需引入spring-boot-starter-data-rest的依赖,无须任何配置即可使用。
Spring boot通过在application.properties中配置以spring.data.rest为前缀的属性来配置RepositoryRestConfiguration。
Spring Data REST实战
新建SpringBoot项目,与上例类似,依赖在前面的基础上增加REST(spring-boot-starter-data-rest)。application.properties的配置信息与前面一样。
添加同样的实体类Person.java,并定义实体类的Repository。其中,在Repository中的方法上添加注解@RestResource可以将方法暴漏为REST源。
package com.wyk.datarestdemo.repository;
import com.wyk.datarestdemo.bean.Person;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RestResource;
public interface PersonRepository extends JpaRepository {
@RestResource(path="nameStartsWith", rel="nameStartsWith")
Person findByNameStartsWith(String name);
}
为了方便测试,我们使用一个工具Postman,可以在官网直接下载。
运行程序,打开Postman,在其中使用GET访问http://localhost:8080/persons/1, 收到如下返回。
使用GET访问地址 http://localhost:8080/persons/search/nameStartsWith?name=Li, 用来测试方法。
Spring Data Rest 还支持分页和排序,以及更新、保存、删除等多个操作,这里不再展示。
Spring Data Rest定制
定制根路径
前面提到相关配置都在application.properties中配置以spring.data.rest为前缀的属性来配置。默认访问路径是根路径,如果想修改,可以进行如下配置。
spring.data.rest.base-path=/api
定制节点路径
在上面的例子,我们使用 http://localhost:8080/persons 访问,这时Spring Data REST的默认规则,使用实体类加s形成路径。如果想对映射名称进行修改,需要在实体类Repository上使用@RepositoryRestResource 注解的path属性进行修改。
@RepsositoryRestResource(path="people")
public interface PersonRepository extends JpaRepository {
@RestResource(path="nameStartsWith", rel="nameStartsWith")
Person findByNameStartsWith(String name);
}
这样访问地址就变成了 http://localhost:8080/api/people 。
声明式事务
Spring事务机制
Spring的事务机制是用统一的机制来处理不同数据访问技术的事务处理。Spring的事务机制提供了一个PlatformTransactionManager接口,不同的数据访问技术的事务使用不同的接口实现。
数据库访问技术 | 实现 |
---|---|
JDBC | DataSourceTransactionManager |
JPA | JpaTransactionManager |
Hibernate | HibernateTransactionManager |
JDO | JdoTransactionManager |
分布式事务 | JtaTransactionManager |
注解事务行为
Spring支持声明式事务。即使用注解来选择需要使用事务的方法。它使用@Transactional注解在方法上表明该方法需要事务支持。如果@Transactional注解使用在类上,则此类的所有public方法都是开启事务的。
Spring提供了一个@EnableTransactionManagement注解在配置类上开启声明式事务支持。使用方式:
@Configuration
@EnableTransactionManagement
public class AppConfig {
...
}
@Transactional的属性如下表。
参数名称 | 功能描述 | 默认值 |
---|---|---|
readOnly | 该属性用于设置当前事务是否为只读事务 | false |
rollbackFor | 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。 | Throwble的子类 |
noRollbackFor | 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。 | Throwble的子类 |
propagation | 该属性用于设置事务的传播行为。 | REQUIRED |
isolation | 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置。 | DEFAULT |
timeout | 该属性用于设置事务的超时秒数 | TIMEOUT_DEFAULT |
SpringBoot事务支持
Spring Data JPA对所有默认的方法都开启了事务支持,且查询类事务默认启用readOnly=true属性。
Spring Boot会自动配置事务管理器,且会自动开启注解事务的支持。
使用和前面相同的例子,创建一个实体类Person和接口PersonRepository。
添加业务服务接口。
package com.wyk.datarestdemo.service;
import com.wyk.datarestdemo.bean.Person;
public interface DemoService {
public Person savePersonWithRollBack(Person person);
public Person savePersonWithoutRollBack(Person person);
}
添加业务服务实现。
package com.wyk.datarestdemo.service.impl;
import com.wyk.datarestdemo.bean.Person;
import com.wyk.datarestdemo.repository.PersonRepository;
import com.wyk.datarestdemo.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class DemoServiceImpl implements DemoService {
@Autowired
PersonRepository personRepository;
@Transactional(rollbackFor={IllegalArgumentException.class})
public Person savePersonWithRollBack(Person person) {
Person p = personRepository.save(person);
if(person.getName().equals("wyk")) {
throw new IllegalArgumentException("wyk已存在,数据将回滚");
}
return p;
}
@Transactional(noRollbackFor = {IllegalArgumentException.class})
public Person savePersonWithoutRollBack(Person person) {
Person p = personRepository.save(person);
if(person.getName().equals("wyk")) {
throw new IllegalArgumentException("wyk虽已存在,数据不会回滚");
}
return p;
}
}
添加控制器。
package com.wyk.datarestdemo.controller;
import com.wyk.datarestdemo.bean.Person;
import com.wyk.datarestdemo.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class MyController {
@Autowired
DemoService demoService;
@RequestMapping("/rollback")
public Person rollback(Person person) {
return demoService.savePersonWithRollBack(person);
}
@RequestMapping("/norollback")
public Person noRollback(Person person) {
return demoService.savePersonWithoutRollBack(person);
}
}
运行程序,访问 http://localhost:8080/rollback?name=wyk&age=29 ,这时程序抛出异常。
java.lang.IllegalArgumentException: wyk已存在,数据将回滚
at com.wyk.datarestdemo.service.impl.DemoServiceImpl.savePersonWithRollBack(DemoServiceImpl.java:20) ~[classes/:na]
at com.wyk.datarestdemo.service.impl.DemoServiceImpl$$FastClassBySpringCGLIB$$2ef6f418.invoke() ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.4.RELEASE.jar:5.1.4.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:749) ~[spring-aop-5.1.4.RELEASE.jar:5.1.4.RELEASE]
...
查看数据库。
mysql> select * from t_person;
+----+----------+------+----------+
| id | name | age | address |
+----+----------+------+----------+
| 1 | xiaoming | 22 | Beijing |
| 2 | xiaohong | 21 | Beijing |
| 3 | Peter | 18 | New York |
| 4 | Jingjing | 18 | Hengshui |
| 5 | Lily | 28 | Tianjin |
| 6 | ss | 25 | Shanghai |
+----+----------+------+----------+
6 rows in set (0.00 sec)
并没有插入成功。
改为访问 http://localhost:8080/norollback?name=wyk&age=29 ,这时程序同样抛出异常。
java.lang.IllegalArgumentException: wyk虽已存在,数据不会回滚
at com.wyk.datarestdemo.service.impl.DemoServiceImpl.savePersonWithoutRollBack(DemoServiceImpl.java:30) ~[classes/:na]
at com.wyk.datarestdemo.service.impl.DemoServiceImpl$$FastClassBySpringCGLIB$$2ef6f418.invoke() ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.1.4.RELEASE.jar:5.1.4.RELEASE]
'''
查看数据库,发现语句已经插入成功。
mysql> select * from t_person;
+----+----------+------+----------+
| id | name | age | address |
+----+----------+------+----------+
| 1 | xiaoming | 22 | Beijing |
| 2 | xiaohong | 21 | Beijing |
| 3 | Peter | 18 | New York |
| 4 | Jingjing | 18 | Hengshui |
| 5 | Lily | 28 | Tianjin |
| 6 | ss | 25 | Shanghai |
| 10 | wyk | 29 | NULL |
+----+----------+------+----------+
7 rows in set (0.00 sec)
Spring 数据缓存
缓存是实际工作中非经常常使用的一种提高性能的方法, 我们会在很多场景下来使用缓存。
Spring 缓存支持
Spring 定义了 org.springframework.cache.CacheMananger 和 org.springframework.cache.Cache 接口用来统一不同的缓存技术。其中,CacheManager 是 Spring 提供的各种缓存技术的抽象接口, Cache 接口包含缓存的各种操作(一般不直接使用)。
Spring 支持的 CacheManager
针对不同的缓存技术,需要实现不同的 CacheManger,Spring 定义了多个 CacheManager 实现。
CacheManager | 描述 |
---|---|
SimpleCacheManager | 使用简单的 Collection 来存储缓存,主要用来测试用途 |
ConcurrentMapCacheManager | 使用 ConcurrentMap 来存储缓存 |
NoOpCacheManager | 仅测试用途,不会实际存储缓存 |
EhCacheCacheManager | 使用 EhCache 作为缓存技术 |
GuavaCacheManger | 使用 Google Guava 的 GuavaCache 作为缓存技术 |
HazelcastCacheManager | 使用 Hazelcast 作为缓存 |
JCacheCacheManager | 使用 JCache 标准的实现作为缓存技术 |
RedisCacheManager | 使用Redis作为缓存技术 |
在我们使用任意一个实现的 CacheManager 的时候,需注册实现 CacheManager 的 Bean。
@Bean
public EhCacheCacheManager cacheManager(CacheManager ehCacheCacheManager) {
return new EhCacheCacheManager(ehCacheCacheManager);
}
声明式缓存注解
Spring 提供了4个注解来声明缓存规则。
注解 | 作用 |
---|---|
@Cacheable | 方法执行前,先从缓存中读取数据,如果缓存没有找到数据,再调用方法获取数据,然后把数据添加到缓存中 |
@CachePut | 调用方法时会自动把方法返回的相应数据放入缓存 |
@CacheEvict | 调用方法时会从缓存中移除相应的数据 |
@Caching | 组合多个注解策略在一个方法上 |
@Cacheable、@CachePut、@CacheEvit 都有 value 属性,指定缓存名称,key 属性指定的是数据在缓存中的存储的键。
开启声明式缓存支持
在配置类上使用 @EnableCaching 即可开启声明式缓存支持。
@Condiguration
@EnableCaching
public class AppConfig {
}
Spring Boot 缓存支持
在 Spring Boot 中已经自动配置了多个 CacheManager 的实现。在不做任何额外配置的情况下默认使用 SimpleCacheManager。Spring Boot 支持以 spring.cache 为前缀的属性来配置缓存。
spring.cache.type= # 缓存类型
spring.cache.cache-names= # 程序启动时创建缓存名称
spring.cache.ehcache.config= # ehcache配置文件地址
spring.cache.hazelcast.config= # hazelcast配置文件地址
spring.cache.jcache.provider= # 当多个 jcache 实现在类路径的时候,指定 jcache 实现
spring.cache.guava.spec= # guava specs
在 Spring Boot 环境下,只需要在项目中导入相关缓存技术的依赖包,并在配置类上使用 @EnableConfig 开启缓存支持即可。
新建 Spring Boot 项目,添加依赖至 pom.xml 。
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-jdbc
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-cache
mysql
mysql-connector-java
runtime
org.springframework.boot
spring-boot-starter-test
test
在配置文件 application.properties 中添加数据库配置信息。
# 数据库相关
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springstudy
spring.datasource.username=spring
spring.datasource.password=spring
spring.datasource.dirverClassName=com.mysql.jdbc.Driver
创建和前面相同的实体类。
package com.wyk.cachedemo.bean;
import javax.persistence.*;
@Entity
@Table(name = "t_person")
public class Person {
@Id //映射为数据库主键
@GeneratedValue(strategy = GenerationType.IDENTITY) // 生成方式为自增
private Long id;
private String name;
private Integer age;
private String address;
public Person() {}
public Person(Long id, String name, Integer age, String address) {
this.id = id;
this.name = name;
this.age = age;
this.address = address;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
添加实体类的 Repository 。
package com.wyk.cachedemo.repository;
import com.wyk.cachedemo.bean.Person;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PersonRepository extends JpaRepository {
}
添加业务服务接口:
package com.wyk.cachedemo.service;
import com.wyk.cachedemo.bean.Person;
public interface DemoService {
public Person save(Person person);
public void remove(Long id);
public Person findOne(Person person);
}
添加业务服务实现:
package com.wyk.cachedemo.service.impl;
import com.wyk.cachedemo.repository.PersonRepository;
import com.wyk.cachedemo.bean.Person;
import com.wyk.cachedemo.service.DemoService;
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.stereotype.Service;
import java.util.Optional;
@Service
public class DemoServiceImpl implements DemoService {
@Autowired
PersonRepository personRepository;
@Override
@CachePut(value="people", key="#person.id")
public Person save(Person person) {
Person p = personRepository.save(person);
System.out.println("为 id、key为" + p.getId() + "数据做了缓存");
return p;
}
@Override
@CacheEvict(value="people")
public void remove(Long id) {
System.out.println("删除了id、key为" + id + "的数据库缓存");
personRepository.deleteById(id);
}
@Override
@Cacheable(value="people", key="#person.id")
public Person findOne(Person person) {
Optional p = personRepository.findById(person.getId());
System.out.println("为 id、key为" + person.getId() + "数据做了缓存");
if(p.isPresent()) {
return p.get();
} else {
return null;
}
}
}
添加控制器:
package com.wyk.cachedemo.controller;
import com.wyk.cachedemo.bean.Person;
import com.wyk.cachedemo.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CacheController {
@Autowired
DemoService demoService;
@RequestMapping("/put")
public Person put(Person person) {
return demoService.save(person);
}
@RequestMapping("/able")
public Person cacheable(Person person) {
return demoService.findOne(person);
}
@RequestMapping("/evit")
public String evit(Long id) {
demoService.remove(id);
return "ok";
}
}
开启缓存支持。
package com.wyk.cachedemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class, args);
}
}
运行程序。首先访问 http://localhost:8080/able?id=1 。
第一次访问会在控制台打印“为 id、key为1数据做了缓存”,后面再次访问则不会,说明已经存在于缓存中。
访问 http://localhost:8080/put?name=tt&age=26&address=Hebei 。
在访问 http://localhost:8080/able?id=11 ,控制台无输出,会直接获得数据。
访问 http://localhost:8080/evit?id=11 ,会删除数据及其缓存。
切换缓存技术
切换缓存只需要在 pom.xml 中添加相应的依赖即可,如果需要配置文件,则在类路径下进行配置, Spring 会自动扫描。
如果我们需要使用 Guava 作为缓存技术,只需要在 pom.xml 中增加 Guava 依赖。
com.google.guava
guava
18.0
非关系型数据库 NoSQL
NoSQL 是对不使用关系作为数据管理的数据库系统的统称,NoSQL 的主要特点是不使用 SQL 作为查询语言,数据存储也不是固定的表、字段。
NoSQL 数据库主要有文档存储型(MongoDB)、图形关系存储型(Neo4j)和键值对存储型(Redis)。
MongoDB
Spring 支持
Spring 对 MongoDB 的支持主要通过 Spring Data MongoDB来实现,Spring Data MongoDB提供了如下功能。
Object/Document 映射注解支持
Spring Data MongoDB 提供如下注解。
注解 | 作用 |
---|---|
@Document | 映射领域对象与MongoDB的一个文档 |
@Id | 映射当前属性为ID |
@DbRef | 当前属性将参考其它文档 |
@Filed | 为文档的属性定义名称 |
@Version | 将当前属性作为版本 |
@Indexed | 用于字段,表示该字段需要如何创建索引 |
@CompoundIndex | 用于类,以声明复合索引 |
@GeoSpatialIndexed | 用于字段,进行地理位置索引 |
@TextIndexed | 用于字段,标记该字段要包含在文本索引中 |
@Language | 用于字段,以设置文本索引的语言覆盖属性 |
@Transient | 默认情况下,所有私有字段都映射到文档,此注解将会去除此字段的映射 |
@PersistenceConstructor | 标记一个给定的构造函数,即使是一个protected修饰的,在从数据库实例化对象时使用。构造函数参数通过名称映射到检索的DBObject中的键值 |
MongoTemplate
MongoTemplate 提供了数据访问的方法,我们还需要为 MongoClient 以及 MongoDbFactory来配置数据库连接属性。
@Bean
public MongoClient client() throws UnknowHostException {
MongoClient client = new MongoClient(new ServerAddress("127.0.0.1",27017));
return client;
}
@Bean
public MongoDbFactory mongoDbFactory() throws Exception {
String database = new MongoCientURI("mongodb://localhost/test").getDataBase();
return new SimpleMongoDbFactory(client(), database);
}
@Bean
public MongoTemplate mongoTemplate(MongoDbFactory mongoDbFactory) throws UnkownHostException {
return new MongoTemplate(mongoDbFactory);
}
Repository 支持
Spring Data MongoDB 还提供了 Repostiory 的支持,使用方式和 Spring Data JPA 一致。
public interface PersonRepository extends MongoRepository {
}
MongoDB 的 Repository 的支持开启需在配置类上注解 @EnableMongoRepositories。
@Configuration
@EnableMongoRepositories
public class AppConfig {
}
Spring Boot 支持
在 Spring Boot 下使用 MongoDB, 只需要引入 spring-boot-starter-data-mongodb 依赖即可,无需任何配置。
其中 MongoDB 相关的信息可以在 application.properties 中以 spring.data.mongodb 为前缀进行配置。
创建项目,添加 Web 和 MongoDB 依赖,添加如下数据库配置信息。
spring.data.mongodb.host=localhost
spring.data.mongodb.port=27017
spring.data.mongodb.database=spring
spring.data.mongodb.username=wyk
spring.data.mongodb.password=123456
添加 Person 实体类,与前面不同的是添加了一个 location 字段。
package com.wyk.mongotest.bean;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.Field;
import java.util.Collection;
import java.util.LinkedHashSet;
@Document
public class Person {
@Id
private String id;
private String name;
private Integer age;
@Field("locs")
private Collection location = new LinkedHashSet();
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Collection getLocation() {
return location;
}
public void setLocation(Collection location) {
this.location = location;
}
}
其中 Location 类的定义如下。
package com.wyk.mongotest.bean;
public class Location {
private String place;
private String year;
public Location(String place, String year) {
this.place = place;
this.year = year;
}
public String getPlace() {
return place;
}
public void setPlace(String place) {
this.place = place;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
}
在 repository 中添加数据访问方法。
package com.wyk.mongotest.repository;
import com.wyk.mongotest.bean.Person;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.data.mongodb.repository.Query;
import java.util.List;
public interface PersonRepository extends MongoRepository {
Person findByName(String name);
@Query("{'age':?0}")
List withQueryFindByAge(Integer age);
}
添加控制器。
package com.wyk.mongotest.contronller;
import com.wyk.mongotest.bean.Location;
import com.wyk.mongotest.bean.Person;
import com.wyk.mongotest.repository.PersonRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
@RestController
public class DataController {
@Autowired
PersonRepository personRepository;
@RequestMapping("/save")
public Person save() {
Person p = new Person("wyk", 30);
Collection locations = new LinkedHashSet<>();
Location loc1 = new Location("河北", "2006");
Location loc2 = new Location("北京","2009");
locations.add(loc1);
locations.add(loc2);
p.setLocation(locations);
return personRepository.save(p);
}
@RequestMapping("/q1")
public Person q1(String name) {
return personRepository.findByName(name);
}
@RequestMapping("/q2")
public List q2(Integer age) {
return personRepository.withQueryFindByAge(age);
}
}
运行程序,访问 http://localhost:8080/save 保存数据。
还可以访问 http://localhost:8080/q1?name=wyk 和 http://localhost:8080/q2?age=30 查看查询结果,这里不再演示。
Redis
待补充。