注意事项(踩坑):
这个项目本来引入的依赖是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自带了findById
、deleteById
、existById
方法,使用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=zipCode
的 Person
。
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);
}
上面的解析方式大多数情况下都能解析出正确结果,但是有时候却不能。假设 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);