使用spring-data-jpa访问数据库

简介: 主要介绍specification和exampleMatcher的使用方式。

注意事项(踩坑):
这个项目本来引入的依赖是spring-data-commons,从官网文档描述来看,spring-data-commons应该和slf4j一样,只提供一组抽象接口,需要配合具体实现来操作数据库。我用的实现是hibernate。这里有个坑,就是不要同时引入互斥的spring-data-commons依赖和spring-data-jpa依赖。我本地实践中,第一版同时引入了spring-data-commons-3.0.1和spring-data-jpa-2.2.11.RELEASE,导致JpaRepository接口中没有findById和save方法,从而导致编译报错。重新引入和spring-data-jpa-2.2.11.RELEASE匹配的spring-data-commons-2.2.11.RELEASE后此问题解决:

红线部分需要匹配,我的第一版代码不匹配,所以出现了依赖冲突,导致编译不通过。删除冲突的spring-data-commons依赖后,问题解决。

环境搭建

数据库准备

使用mysql数据库,新建一个user表:

create schema if not exists spring_data;
create table if not exists spring_data.user
(
  id varchar(16) primary key not null,
  gender varchar(2) not null,
  name varchar(16) not null,
  age int not null,
  location varchar(64),
  country varchar(16)
);

spring和jpa配置

创建完成后就可以配置spring和spring-jpa了。可以使用java配置,也可以使用xml配置。下面是java配置示例。可以点击xml配置来查看等价的xml配置写法。

package com.wrotecode.springdata.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Properties;

@Configuration
@EnableJpaRepositories("com.wrotecode.springdata.repository")
public class DbConfig {
   
   

    @Bean("dataSource")
    public DataSource getDataSource() {
   
   
        // 这里使用spring自带的连接池,实际项目中,我们可以使用其他数据源,比如druid或者dbcp等。
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        // 注意事项:生产环境禁止使用明文配置,我们这是学习demo,写成明文无所谓。
        dataSource.setUrl("jdbc:mysql://localhost:3306/spring_data");
        dataSource.setUsername("root");
        dataSource.setPassword("123456");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

    @Bean("entityManagerFactory")
    public EntityManagerFactory getEntityManagerFactory() throws IOException {
   
   
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();

        factoryBean.setDataSource(getDataSource());

        JpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter);

        factoryBean.setPackagesToScan("com.wrotecode.springdata.entity");

        Properties properties = new Properties();
        properties.load(new ClassPathResource("jpa.properties").getInputStream());
        factoryBean.afterPropertiesSet();
        return factoryBean.getObject();
    }

    @Bean("transactionManager")
    public JpaTransactionManager getTransactionManager() throws IOException {
   
   
        JpaTransactionManager jpaTransactionManager = new JpaTransactionManager();
        jpaTransactionManager.setEntityManagerFactory(getEntityManagerFactory());
        return jpaTransactionManager;
    }
}

使用spring query method访问数据库

通常,在jpa中,访问数据库的方式是构建criteria查询。spring提供了一种全新的访问数据库的方式,那就是query method。以本文中的user为例,jpa自带了findByIddeleteByIdexistById方法,使用query method后,还可以以同样的语法来定义其他字段,比如findByAge。此外,在定义query method时,idea会进行智能提示:

query method定义在继承了JpaRepository 接口的接口中。在方法名中定义的参数需要和方法实际参数匹配。如果定义了findByIdAndAge方法,那么参数中就必须有id参数和age参数,否则会抛出异常。

查询生成策略

在定义查询方法后,下一步就是生成查询语句。查询语句的生成策略是一个重要的问题。生成策略必须足够好,这样才能保证所有的查询方法都能被正确解析。

spring 中可以手动设置生成策略。如果使用 xml 配置,可以使在命名空间中使用query-lookup-strategy 属性来定义生成策略,如果是 java 配置,可以在 @EnableJpaRepositories 中使用 queryLookupStrategy 属性来定义生成策略。注意:一些数据库可能不支持特定的生成策略。spring 中有下面三种生成策略:

  • CREATE:该策略会试图创建一个 SQL 查询(其实是 JPA 查询),即增删改查。通常,这个策略会截取查询方法中的通用前后缀,然后解析剩余部分。比如上面的 findAllByIdAndNameOrderByIdDesc,会截取 findByAll 和 OrderByIdDesc,然后解析剩下的 ByIdAndName。
  • USE_DECLARED_QUERY:查找命名查询,如果没有找到,就抛出异常。命名查询可以参考示例代码。除了使用注解定义命名,还可以使用其他方式来定义。
  • CREATE_IF_NOT_FOUND:默认策略,结合前两种策略。该策略首先会查找已定义的命名查询,如果没有找到,则会创建一个查询。

当我们定义查询方法后,spring 会根据我们定义的方法来生成查询语句,这里就会有个问题,如果一个字段包含多个单词,spring 会如何区分这些单词是不同的字段还是一个字段的不同组成部分?

创建查询方法

spring-data-jpa 提供的功能对创建基于实体的查询很有用。下面代码是查询示例:

interface PersonRepository extends Repository<Person, Long> {
   
   

  List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

  // Enables the distinct flag for the query
  List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
  List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

  // Enabling ignoring case for an individual property
  List<Person> findByLastnameIgnoreCase(String lastname);
  // Enabling ignoring case for all suitable properties
  List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

  // Enabling static ORDER BY for a query
  List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
  List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

一个查询方法被分成两部分,第一部分是查询关键字,用来说明此方法的查询目标是用来增/删/改/查。查询关键字除了说明查询目标外,还可以增加排序、去重等关键字。查询方法的第二部分是查询条件。查询关键字可以参考关键字。除了查询关键字外,条件关键字还可以参考条件关键字

属性表达式

属性表达式只能引用使用由 jpa 管理的类种的属性,即使用@Entiry 注解的类。属性表达式也可以解析嵌套属性。假设 Person类中有一个 Address 类型的字段,Address 类中有一个字符串类型的 zipCode 字段,那么可以使用 findByAddressZipCode(String zipCode) 来查找 person.address.zipCode=zipCodePerson

spring 解析这种嵌套属性的方式如下:先将所有嵌套字段作为一个整体去匹配实体类中是否有该属性,如果没有属性,则按照驼峰命名法,将嵌套属性分割,然后寻找实体类是否有第一个字段,如果没有,则将第一个单词和第二个单词视作一个整体去实体类中匹配,如果没有匹配到,则将前三个单词作为一个整体去匹配。依此类推。在本例中,spring 首先查找 addressZipCode 字段,发现 Person 中没有此字段,则使用 address 去匹配,匹配到后,再到 address 中匹配 zipCode 字段,匹配成功,得到的嵌套属性就是 person.address.zipCode。下面时测试代码:

    @Test
    public void testOneToOne() {
   
   
        String personId = IdUtil.nextId();
        String addressId = IdUtil.nextId();
        String zipCode = String.valueOf(System.currentTimeMillis());
        Person person = new Person();
        person.setId(personId);
        Address address = new Address();
        address.setId(addressId);
        address.setZipCode(zipCode);
        person.setAddress(address);
        // 如果直接保存person而不保存address,会抛出异常:找不到addressId对应的address
        addressRepository.save(address);
        Person person2 = personRepository.save(person);
        System.out.println(person2);
        Person person3 = personRepository.findByAddressZipCode(zipCode);
        System.out.println(person3);
    }

实体类可以参考 PersonAddress

上面的解析方式大多数情况下都能解析出正确结果,但是有时候却不能。假设 Person 中还有一个 addressZip 属性,那么上面的算法会生成 person.addressZip.code,很明显这是一个错误的表达式,因为 Address 中没有 code 属性。此时我们可以通过下划线来手动分割属性:findByAddress_ZipCode(String zipCode)。抛出的异常如下:

Caused by: org.springframework.data.mapping.PropertyReferenceException: No property code found for type Address! Traversed path: Person.addressZip.
    at org.springframework.data.mapping.PropertyPath.<init>(PropertyPath.java:94)
    at org.springframework.data.mapping.PropertyPath.create(PropertyPath.java:382)
........省略

使用specification进行数据库交互

在jpa中引入了一种criteria接口,这种接口可以在不写sql的情况下来完成条件查询,最长用的就是描述jpa查询的where条件,以及是否去重活使用聚合函数等。spring将criteria接口做了一层抽象,将他们封装在specification接口中,用户可以通过specification接口来编写出简单的criteria查询语句。specification接口结构如下:

以上面的user表为例,如果我们想要查询年龄大于20岁的用户,可以编写以下代码

    @Test
    public void demo() {
   
   
        Specification<User> specification = (root, query, builder) -> {
   
   
            Predicate predicate = builder.ge(root.get(User_.age), 20);
            return predicate;
        };
        List<User> all = repository.findAll(specification);
        System.out.println(all);
    }

如果不想用specification接口,只想用原生criteria接口,会如何呢?下面是使用原生criteria api的写法:

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
        CriteriaQuery<User> query = criteriaBuilder.createQuery(User.class);
        Root<User> root = query.from(User.class);
        Predicate predicate = criteriaBuilder.ge(root.get(User_.age), 20);
        query.where(predicate);
        List<User> resultList = entityManager.createQuery(query).getResultList();
        System.out.println(resultList);

可以看到,specification大大简化了criteria接口的使用方法。在原生criertia api中,要执行查询,首先需要从实体管理器中获取一个CriteriaBuilder,然后使用这个builder来创建一个CriteriaQuery。注意,这时候还不能执行查询。之后使用CriteriaQuery生成一个Root,最后使用CriteriaBuilder来构造查询条件,然后将他们组合在一起。可以看到,在spring specifiation接口下,我们通常只需要构造查询条件,剩下的内容交给spring执行就行。其实,不管是使用原生criteria api还是使用specification,整个操作流程是固定的,区别是,使用criteria是需要我们完成所有流程,而是用specification时,我们只需要购兼查询条件,剩下的spring会代替我们高效执行。

使用ExampleMatcher和数据交互

ExampleMatcher是另一种和数据库交互的方式。使用specification,就像用java写sql一样。使用exampleMatcher,也是写sql,只不过传给sql的不是值,而是一个对象,可以参考示例写法

// 创建一个对象,这个对象的值就是我们查找的条件
User user = new User();
user.setGender("1");
user.setName("username");
// 匹配任意条件。在这个代码中,就是匹配有值的字段。默认情况下是匹配所有条件,即匹配所有字段。
// 匹配所有字段时,若user中username属性为空,则查表会使用username is null,导致查询不到任何数据,因此需要匹配任意条件。
ExampleMatcher matcher = ExampleMatcher.matchingAny()
    // 以对象中的用户名开始
    .withMatcher("username", GenericPropertyMatchers.startsWith())
    // 精确匹配
    .withMatcher("gender", ele -> ele.exact());
// 创建一个新的匹配对象
Example<User> example = Example.of(user, matcher);
// 进行查询
List<User> all = repository.findAll(example);
System.out.println(all);
相关文章
|
4月前
|
存储 前端开发 Java
Spring Boot中Spring MVC的表单标签库与数据绑定讲解与实战(附源码 超详细必看)
Spring Boot中Spring MVC的表单标签库与数据绑定讲解与实战(附源码 超详细必看)
33 0
|
4月前
|
前端开发 Java 数据库
【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(下)
【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(下)
|
4月前
|
Java 数据库连接 数据库
【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(中)
【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(中)
135 0
|
4月前
|
Java 关系型数据库 MySQL
【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战(上)
【Spring AOP + 自定义注解 + 动态数据源 实现主从库切换&读写分离】—— 案例实战
|
7月前
|
Java 数据库连接 数据库
Spring 应用如何访问数据库,看这一篇就够了!
当我们开发应用时,访问数据库是一种常见的需求。 基本上所有需要持久化的数据,一般都存储在数据库中,例如常用的开源数据库 MySQL。 在今天的文章中,我将盘点一下 Java 应用访问数据的几种方式。
34017 9
|
7月前
|
Java 数据库连接 API
Spring Boot 中如何使用 Spring Data JPA 来访问数据库?
Spring Boot 中如何使用 Spring Data JPA 来访问数据库?
|
11月前
|
前端开发 Java 应用服务中间件
Spring MVC-05循序渐进之数据绑定和form标签库(下) 实战从0到1
Spring MVC-05循序渐进之数据绑定和form标签库(下) 实战从0到1
682 0
|
11月前
|
前端开发 Java 数据安全/隐私保护
Spring MVC-05循序渐进之数据绑定和form标签库(上)
Spring MVC-05循序渐进之数据绑定和form标签库(上)
10282 0
|
11月前
|
SQL 存储 Oracle
Spring JDBC-使用Spring JDBC访问数据库
Spring JDBC-使用Spring JDBC访问数据库
109 0
|
XML 开发框架 监控

热门文章

最新文章