Spring Boot学习笔记(四):Spring Boot 数据访问

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):

Spring Boot学习笔记(四):Spring Boot 数据访问_第1张图片
springboot-jpa-save.png

根据属性查询(http://localhost:8080/q1?name=xiaoming):

Spring Boot学习笔记(四):Spring Boot 数据访问_第2张图片
springboot-jpa-q1.png

根据多条属性查询(http://localhost:8080/q2?name=xiaoming&address=Beijing):

Spring Boot学习笔记(四):Spring Boot 数据访问_第3张图片
springboot-jpa-q2.png

根据@Query注解查询(http://localhost:8080/q3?name=xiaoming&address=Beijing):

Spring Boot学习笔记(四):Spring Boot 数据访问_第4张图片
springboot-jpa-q3.png

根据NamedQuery查询(http://localhost:8080/q4?name=xiaoming&address=Beijing):

Spring Boot学习笔记(四):Spring Boot 数据访问_第5张图片
springboot-jpa-q4.png

查询结果排序(http://localhost:8080/sort):

Spring Boot学习笔记(四):Spring Boot 数据访问_第6张图片
springboot-jpa-sort.png

查询结果分页(http://localhost:8080/sort):

Spring Boot学习笔记(四):Spring Boot 数据访问_第7张图片
springboot-jpa-page.png

自定义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 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, 无查询条件,查看结果。

Spring Boot学习笔记(四):Spring Boot 数据访问_第8张图片
springboot-jpa-auto1.png

访问http://localhost:8080/auto?address=h,进行模糊查询。

Spring Boot学习笔记(四):Spring Boot 数据访问_第9张图片
springboot-jpa-auto2.png

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, 收到如下返回。

Spring Boot学习笔记(四):Spring Boot 数据访问_第10张图片
springboot-rest-1.png

使用GET访问地址 http://localhost:8080/persons/search/nameStartsWith?name=Li, 用来测试方法。

Spring Boot学习笔记(四):Spring Boot 数据访问_第11张图片
springboot-rest-2.png

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 保存数据。

Spring Boot学习笔记(四):Spring Boot 数据访问_第12张图片
springboot-jpa-save.png

还可以访问 http://localhost:8080/q1?name=wyk 和 http://localhost:8080/q2?age=30 查看查询结果,这里不再演示。

Redis

待补充。

你可能感兴趣的:(Spring Boot学习笔记(四):Spring Boot 数据访问)