Spring Data JPA 学习笔记1 - JPA与Spring Data
标记【跳过】的未来完善
1 理解JPA
1.1 什么是持久化?
当一个软件关闭的时候,软件内储存的状态数据还能在下次开启时被恢复,这就是持久化。对象持久化是指每个独立的对象的生命周期都能不依赖应用程序进程,比如将对象存储到数据库或者在以后能被重新创建。在Java当中,持久化是指使用SQL语句在数据库种映射和储存对象。
1.2 范式不匹配问题
Java中对象的存储和传统SQL数据库中存储信息的方式完全不同,既然我们要把Java对象存储到数据库中,我们就需要解决Java对象与数据库数据条目的范式不匹配问题。对象与数据库数据范式不匹配主要有以下几个方面
-
信息粒度不匹配
Java能很容易的存储一个含有地址对象属性的用户对象,地址对象可以包含很多属性。Java的这种结构很难确定好数据粒度,而SQL数据库以表的形式存储最小数据粒度就是一个行键和列确定的单元格的值。
-
继承/实现关系不匹配
SQL数据库没办法实现面向对象中的继承和多态性。
-
比较方式不匹配
SQL数据库用主键来区分不同的数据,而Java使用
==
、equals()
或者compareTo()
等来确定两个对象是否一致。 -
关联方式不匹配
面向对象程序通过对象引用来实现关联,但是关系型数据库使用外键来关联两个实例。
面向对象中的关联是有向的,如果想要双向需要在关联的两头都声明关联。关系型数据库完全没有关联方向,需要关联则需要
join
和projection
操作Java可以很容易实现多对多关联,比如一个学生有多个老师,这些老师同时也教多个学生,只需要两边都写个对方类对象的集合就可以,但是关系型数据库中需要一张单独的关联表
-
浏览数据方式不匹配
Java中使用存储的指针来在多个类的对象之间获取数据,而SQL数据库需要多个表的
join
来获得,如果类对象的关联网比较复杂就需要join
更多张表
1.3 ORM和JPA
ORM(Object/Relational Mapping)对象关系映射是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象数据和对象之间的关联自动持久化到关系数据库中的一种操作。
JPA(Java Persistence API)Java持久化层API是Java语言中实现ORM的一个标准,实现JPA规范的ORM框架,可以帮助实现对象的持久化操作。
JPA规范定义了:
- 一套具体映射元数据的标准,元数据指明了持久化类结构和他们具有的属性与数据库表之间的关联
- 一些用于执行CRUD操作的API接口,省去编写重复JDBC和SQL代码的麻烦
- 一套查询语言
- 定义了带有事务特征的持久化引擎如何执行脏数据检查、关联查询以及其他的优化函数
Hibernate就是一套实现JPA标准的Java持久化框架,Spring Data JPA就是基于Hibernate实现JPA规范,并提供了简化开发过程的一些功能
2 Spring Data
2.1 核心概念
Spring Data是一个致力于减少数据访问层DAO开发量的项目,开发者只需要定义数据仓库接口继承Spring Data定义好的具有CRUD等数据访问功能的接口,或者按照Spring Data定义的由动词、可选主题、关键词By和断言组成的领域特定语言编写的方法名让Spring Data自动生成具体操作代码和语句,实现编程式数据访问
Spring Data是一个庞大的项目,它包括下面介绍的Spring Data Commons通用核心组件与众多针对不同数据库与不同API规范的子模块,官方的例如支持JPA规范的Spring Data JPA、支持JDBC的Spring Data JDBC、支持MongoDB的Spring Data MongoDB、支持Redis的Spring Data Redis等,社区支持的Spring Data Elasticsearch、Spring Data Aerospike等等,下面介绍的是他们共有的核心Spring Data模块,所有子模块的概念与使用操作都遵循这些规律
Spring Data仓库抽象的核心接口是Repository<T, ID>
,它接受领域类和领域类ID类型作为类型参数,是一个用于获取后续工作所要使用的类型并且帮助你去了解其他扩展接口的标记接口,定义的数据仓库接口继承它就能实现Spring Data根据方法名生成查询操作的功能。以下是几个比较重要的子接口
-
CurdRepository
:为被管理实体实现了许多复杂的CURD功能public interface CrudRepository<T, ID> extends Repository<T, ID> { <S extends T> S save(S entity); // 保存实体 Optional<T> findById(ID primaryKey); // 使用ID参数找到实体 Iterable<T> findAll(); // 获取全部实体 long count(); // 所有实体数量 void delete(T entity); // 删除指定实体 boolean existsById(ID primaryKey); // 是否存在指定ID的实体 // … 还有其他的方法 }
-
PagingAndSortingRepository
:为findAll()
获取实体提供分页能力public interface PagingAndSortingRepository<T, ID> extends CrudRepository<T, ID> { Iterable<T> findAll(Sort sort); Page<T> findAll(Pageable pageable); // 传入PageRequest.of(0, 20)获取第一页共20条实体 }
-
JpaRepository
:实现了JPA规范的相关方法public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> { void flush(); <S extends T> S saveAndFlush(S entity); void deleteInBatch(Iterable<T> entities); void deleteAllInBatch(); T getOne(ID id); // 覆写了一些上层接口的方法 }
2.2 使用步骤
-
定义一个继承
Repository
或者它子接口的接口,传入领域类和领域类ID类型interface PersonRepository extends Repository<Person, Long> { … }
-
在接口里声明符合Spring Data方法声明或接口内已有的方法
interface PersonRepository extends Repository<Person, Long> { List<Person> findByLastname(String lastname); }
-
如果是使用Spring则可以用Java配置的方式创建这些接口的代理,如果是新版Spring Boot那就什么也不用做了
@EnableJpaRepositories class Config { … }
-
然后就可以直接注入自定义的
PersonRepository
使用了class SomeService { private final PersonRepository repository; SomeClient(PersonRepository repository) { // 构造器方式注入 this.repository = repository; } void doSomething() { List<Person> persons = repository.findByLastname("Lee"); } }
下面是每个步骤的详细说明
2.3 定义接口
使用Spring Data的第一步就是定义对应每个领域类的数据仓库(Repository)接口,这个接口必须继承Repository<T, ID>
接口并传入领域类与领域类主键类型。如果想要暴露一些已经定义好的CRUD方法,转而继承CrudRepository
或者其他接口。
2.3.1 仓库定义详解
正常情况下定义的仓库接口会继承Repository
、CrudRepository
或者PagingAndSortingRepository
。如果不想继承Spring Data已经定义好的接口可以仅加上@RepositoryDefinition
并设置domainClass
和idClass
属性,这个注释等同于该接口继承了Repository<T, ID>
,注释上的两个属性对应Repository
接口的T
与ID
参数
在继承例如CrudRepository
接口时,如果希望仅暴露部分接口,可以复制接口内的方法到一个中间接口内,这个接口可以添加@NoRepositoryBean
注释来通知Spring不为该中间接口生成Bean
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {
Optional<T> findById(ID id);
<S extends T> S save(S entity);
}
interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}
仓库定义的方法会被路由到你选择的Spring Data储存的基础数据仓库上,当你使用Spring Data JPA时这个基础仓库会是SimpleJpaRepository
,例子中的UserRepository
就有了用ID查找用户、保存用户、用邮箱地址找到用户的能力
2.3.2 与多个Spring Data模块配合
如果项目需要用到多个Spring Data模组,比如既想用Spring Data JPA也想用Spring Data MongoDB,Spring Data在探测到多个模块时就会进入严格模式,利用Repository的定义来区分哪个仓库用哪个模组。
- 如果自定义的仓库定义直接继承模组对应的仓库接口,则放到指定模组的候选里。接口例如JPA的
JpaRepository
- 如果自定义仓库的领域类被特定模组的注解注释了,则被放到指定模组的候选里。注释例如JPA的
@Entity
和Spring Data MongoDB和Elasticsearch的@Document
注解
// 针对Person是西安一个repository接口
interface PersonRepository extends Repository<Person, Long> { … }
@Entity // 使用JPA的@Entity注释,Spring Data会将该领域类对应接口给Spring Data JPA进行管理
class Person { … }
// 针对User实现一个repository接口
interface UserRepository extends Repository<User, Long> { … }
@Document // 使用@Document注释,Spring Data会将该领域类对应接口给Spring Data MongoDB进行管理
class User { … }
但是上述情况有例外,当使用中间接口时,SpriDat无法完美分辨出针对同一个领域类的继承同样接口的中间接口实现和直接实现,所以避免这种情况。
多个模块整合时还可以在启用对应仓库时添加basePackage
注释
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }
2.4 定义查询方法
Spring Data的数据仓库接口代理有两种将查询方法转换为实际查询的途径,分别是直接解析方法名和解析定义好的查询。
2.4.1 查询方法选择
可以通过在@Enable...Repository
注释上增加queryLookupStrategy
属性来设置特定的Spring Data模块如何解析查询方法。这个属性有三个可以选择的值:
CREATE
:使用方法名生成USE_DECLARED_QUERY
:使用开发者已经生命好的查询语句,如果找不到将会抛错CREATE_IF_NOT_FOUND
:默认使用的方法,结合了两者,首先会查找已经声明好的查询语句,如果找不到就解析方法名
2.4.2 查询语句生成
Spring Data数据仓库架构内的查询语句生成机制会根据需要自动生成对应查询语句。首先它会去掉方法名的find...By
、read...By
、query...By
、count...By
和get...By
等前缀词并解析方法名省下的部分。第一个By
会作为实际查询规则的开头,后面增加的语句可以使用And
和Or
来连接,下面是一些查询语句的例子
interface PersonRepository extends Repository<Person, Long> {
// 使用EmailAddress属性对应字段和Lastname属性对应字段查询Person
List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);
// 使用SQL中的Distinct标签
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);
// 不区分大小写(IgnoreCase)的查询Lastname
List<Person> findByLastnameIgnoreCase(String lastname);
// 所有可以不区分大小写的参数都不区分
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);
// 使用OrderBy排序,Asc是升序,Desc是降序
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}
实际的查询功能与用到的Spring Data模块有关,下面是一些通用的语句写法
And
或者Or
连接条件,使用Between
、LessThan
、GreaterThan
和Like
等来写条件IgnoreCase
忽略大小写,可以分属性写比如findByLastnameIgnoreCase(String Lastname)
也可以应用于所有字符串参数findByLastnameAndFirstnameAllIgnoreCase(String Lastname, String Firstname)
OrderBy
语句用来为查询结果排序,可以选择Asc
升序或者Desc
降序
2.4.3 属性定义的写法
查询方法内属性名称只跟实体的属性名字有关,如果是多层嵌套直接一层一层写就可以了,比如下方的例子中Person
拥有对象属性Address
,约束是Address
内的Zipcode
对象,这就形成了查询Person.address.zipCode
。
List<Person> findByAddressZipCode(ZipCode zipCode);
Spring Data会首先将AddressZipCode
作为一个完整的属性名来解析,如果解析不成功,解析算法会从字段右侧的第一个大写字母开始分开字段来解析(解析为AddressZip
和Code
),如果第一步解析失败则将从左部分开(解析为Address
和ZipCode
)并循环这样做。如果你觉得这样太麻烦,也可以直接用_
下划线将属性名称分开。
List<Person> findByAddress_ZipCode(ZipCode zipCode);
由于Spring Data使用下划线解析方法名的实现,编写实体类的时候不能使用_
下划线在属性名内,可以使用驼峰命名方式。
2.4.4 解析参数方式
除了正常上方例子展示的传参方法(方法名内定义属性对应一个方法的参数),Spring Data还会解析额外的像Pageable
分页参数和Sort
排序参数等动态分页分类查询参数如下面的例子所示。
// 注意添加Pagebale参数的返回值可以用Page、Slice、List来包装
Page<User> findByLastname(String lastname, Pageable pageable);
// 如果数据库内可能的数据条目比较大,使用Slice性能更好,使用Page将调用Count语句统计数据库内数据条数,而Slice不具有这个信息也就不需要调用Count
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
特别注意,如果方法定义了Pageable
和Sort
参数时在某些情况下你不想使用分页和排序了,请不要向方法传入null
,正确的做法是使用Sort.unsorted()
和Pageable.unpaged()
构建参数来告知Spring Data不排序和分页。
2.4.4.1 构建排序分页
构建排序对象非常容易,一个排序对象可以结合多个排序方式
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
不过上述传参排序方式只用属性名可能会出问题,可以使用下面的方式指定类来确保类型安全
TypedSort<Person> person = Sort.sort(Person.class);
TypedSort<Person> sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
如果使用的Spring Data模组支持Querydsl,也可以使用生成的元模型类型来定义
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
Pageable
的构建可以通过Spring MVC参数直接获取或者使用PageRequest.of()
定义一个实现了Pageable
接口的PageRequest
对象。
传给Pageable
的参数有三个page
从0开始的页码、size
每一页的大小默认20、sort
排序方法(在请求中传sort=firstname&sort=lastname,desc
参数表示在按firstname
正序排列基础上按lastname
倒序排列)
@RequestMapping("list")
public Page<T> getEntryByPageable(
@PageableDefault(value = 15, sort = { "id" }, direction = Sort.Direction.DESC) Pageable pageable
) {
return dao.findAll(pageable);
}
PageRequest pageRequest = PageRequest.of(0, 5); // 构造查询第0页每页5个的PageRequest,这个对象可以直接传给声明Pageable参数的方法
PageRequest pageRequest = PageRequest.of(0, 20, sort); // 还可以传sort对象进去
2.4.5 限制查询结果数
查询方法可以使用相同效果的first
或者top
关键词来限制查询条目,first
和top
后面可以添加数字来限制查询条目数量,如果加了first
或top
默认的返回条目会是1。结合这俩关键词与Sort
或者Pageable
可以实现返回末尾元素
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
2.4.6 返回集合和迭代数组
一个返回多个元素的Repository方法可以使用Iterable
、List
、Set
,同时也提供Spring Data的Streamable
一个Iterable
的扩展类型支持。
2.4.6.1 返回流类型
Streamable
可以代替Iterable
或者其他集合类型,它提供了方便的非并行Stream
支持,提供了filter()
、map()
也可以方便的与其他Streamable
集合合并成一个集合
interface PersonRepository extends Repository<Person, Long> {
Streamable<Person> findByFirstnameContaining(String firstname);
Streamable<Person> findByLastnameContaining(String lastname);
}
// 合并了两个查询结果
Streamable<Person> result = repository.findByFirstnameContaining("av")
.and(repository.findByLastnameContaining("ea"));
2.4.6.2自定义流包装类型
为返回多个元素的集合类型提供专属的包装类型是很常见的,我们可以通过实现以下两个标准来让Spring Data返回自定义的包装类型
- 这个类必须实现
Streamable
接口 - 这个类必须有一个接收
Streamable
参数名为of()
或valueOf()
的构造器或静态工厂函数
class Product { // 单个产品元素
MonetaryAmount getPrice() { … }
}
@RequiredArgConstructor(staticName = "of") // 让Lombok为我们生成一个of()构造函数
class Products implements Streamable<Product> { // 实现Streamable接口的集合类型
private Streamable<Product> streamable;
public MonetaryAmount getTotal() { // 向外暴露一些接口
return streamable.stream()
.map(Priced::getPrice)
.reduce(Money.of(0), MonetaryAmount::add);
}
}
interface ProductRepository implements Repository<Product, Long> {
Products findAllByDescriptionContaining(String text); // 直接用Products返回就可以了
}
2.4.7 Null值的处理
Repository里的CRUD方法可以使用Optional
返回集合实例来解决可能的空值问题,Spring Data还支持返回com.google.common.base.Optional
、scala.Option
、io.vavr.control.Option
类型。当Repository内的方法不返回包装类型而是直接返回实体时,如果未查询到就会返回null
。当返回值被定义为集合、包装类型、流类型为空时将会返回空集合而不是null
。
2.4.7.1 处理Null值的注解
Spring框架本身提供了一些处理Null值的注释,当不符合规范时将抛错
@NonNullApi
:用在包上例如package com.exmple
语句上,声明默认接收的参数和返回值不接受null值@NonNull
:用在参数或返回值上,声明不接受null
值@Nullable
:用在参数或返回值上,声明接受null
值
为了启动运行时的null安全检查,需要像下方在package-info.java
添加@NonNullApi
@org.springframework.lang.NonNullApi
package com.acme;
不过当然最好的方式是使用Optional
或集合、包装、流类型,这样就会返回一个空的集合对象,下方展示了处理Null值的不同方式
package com.acme; // 这个包首先定义了上方的注释,然后就可以开始null检查了
import org.springframework.lang.Nullable;
interface UserRepository extends Repository<User, Long> {
// 当查询无结果时抛出EmptyResultDataAccessException,当参数为null时抛出IllegalArgumentException
User getByEmailAddress(EmailAddress emailAddress);
@Nullable // 接受null的返回值
User findByEmailAddress(@Nullable EmailAddress emailAdress); // 接受null参数
// 如果查询无结果时返回Optional.empty(),当参数为null时抛出IllegalArugmentException
Optional<User> findOptionalByEmailAddress(EmailAddress emailAddress);
}
4.4.7 kotlin【跳过】
2.4.8 查询返回流
查询方法返回值可以是Java 8的Stream<T>
@Query("select u from User u")
Stream<User> findAllByCustomQueryAndStream();
Stream<User> readAllByFirstnameNotNull();
@Query("select u from User u")
Stream<User> streamAllPaged(Pageable pageable);
因为Stream
必须在使用后挂壁,所以你可以手动调用close()
或使用try-with-resource
特性来使用流
try (Stream<User> stream = repository.findAllByCustomQueryAndStream()) {
stream.forEach(…);
}
4.4.9 异步查询【跳过】
2.5 创建仓库实例
4.5.1 XML配置【跳过】
2.5.1 JavaConfig配置
在一个Java配置类上使用Spring Data模块对应的@Enable...Repository
注解就可以开启对应数据仓库支持
@Configuration
@EnableJpaRepositories("com.acme.repositories") // 使用Spring Data Jpa
class ApplicationConfiguration {
@Bean
EntityManagerFactory entityManagerFactory() {
// …
}
}
4.6自定义Spring Data Repository实现【跳过】
2.6 从聚集根处订阅事件
当我们执行完一些数据操作后经常需要执行一些其他业务流程,这时候就需要一些方法来让我们获取到数据操作事件。Spring Data仓库管理的实体是聚集根,在聚集根处可以发出领域事件。Spring Data提供了@DomainEvents
注解,这个注解可以用在聚集根上来获取数据处理事件。
class User {
@DomainEvents
Collection<Object> domainEvents() {
// … 返回一个或多个发出的事件
return new UserSaveEvent();
}
@AfterDomainEventPublication // 可选
void callbackMethod() {
// … 可以在这执行一些领域事件发出后执行的回调
}
}
class UserSaveEvent {}
/** 接受User发出的类型为UserSaveEvent的DomainEvents事件
* phase有BEFORE_COMMIT、AFTER_COMMIT、AFTER_ROLLBACK、AFTER_COMPLETION
**/
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void event(UserSaveEvent event){
// 这里就拿到event了
userRepository.getOne();
}
2.7 Spring Data扩展
2.7.1 SpringMVC支持
未使用Spring Boot组合Spring Data与Spring MVC需要在配置类上添加如下注释
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
class WebConfiguration {}
@EnableSpringDataWebSupport
注解注册会自动注册一些下方提到的组件
2.7.1.1 基本Web支持
开启上方提到的注释会注册下方几个基础组件:
DomainClassConverter
给Spring MVC在请求到达时解析在请求参数或路径参数传递的数据仓库管理的领域类的能力HandlerMethodArgumentResolver
的实现,让Spring MVC有解析Pageable
和Sort
实例请求参数的能力
DomainClassConverter
给予Spring MVC从请求参数里直接解析出领域类的能力,这样你就不需要手动与仓库管理的领域类进行交互了
@Controller
@RequestMapping("/users")
class UserController {
@RequestMapping("/{id}")
String showUserForm(
@PathVariable("id") User user, // 直接从路径参数里解析出User领域类
Model model
) {
model.addAttribute("user", user);
return "userForm";
}
}
上方的例子中,参数仅传入了一个用户的id,由于DomainClassConverter
的存在,传入参数将会触发数据仓库执行findById()
操作,并最终获取数据仓库里储存的对应id的实例,但该数据仓库必须要实现CrudRepository
接口
2.7.1.2 分页排序支持
在注册的主键中还有两个针对HandlerMethodArgumentResolver
的实现,分别是给予Spring MVC处理分页能力的PageableHandlerMethodArgumentResolver
和SortHandlerMethodArgumentResolver
。这两个接口实现让Spring MVC的Controller有了解析Pageable
和Sort
参数的能力
@Controller
@RequestMapping("/users")
class UserController {
private final UserRepository repository;
UserController(UserRepository repository) {
this.repository = repository;
}
@RequestMapping
String showUsers(
Model model,
Pageable pageable // Spring MVC现在有能力直接解析Pageable参数
) {
model.addAttribute("users", repository.findAll(pageable)); // 直接使用pageable对象查数据
return "users";
}
}
Pageable
接口接受page
从0开始的页码数、size
默认为20的每页元素数量、sort
排序方式参数,在前面有提到过具体使用方法。
如果想自定义解析器,则可以创建一个实现PageableHandlerMethodArgumentResolverCustomizer
或SortHandlerMethodArgumentResolverCustomizer
接口的Bean,接口内的customize()
方法会被调用给予你修改设置的能力
@Bean SortHandlerMethodArgumentResolverCustomizer sortCustomizer() {
return s -> s.setPropertyDelimiter("<-->");
}
更进一步的完全定制可以继承SpringDataWebConfiguration
重写父类内pageableResolver()
或者sortResolver()
方法,引入自定义的配置文件而不是使用@Enabel*
注解
如果查询多个表时需要传入多个Pageable
和Sort
参数,则需要使用Spring的@Qualifier
注解,此时传入数据的参数名需要带有${qualifier}_
前缀
String showUsers(Model model,
@Qualifier("thing1") Pageable first,
@Qualifier("thing2") Pageable second) { … }
在上例中我们给第一个Pageable
传参page
时,写法是thing1_page=x
默认的Pageable
等于一个PageRequest.of(0, 20)
生成的PageRequest
对象,这个默认值可以通过在Pageable
参数上添加@PageableDefault
注解并传入属性的方式进行定制。
4.8.2 - 超媒体支持【跳过】
2.7.1.3 数据绑定支持
Spring Data投影(Projections)可以使用JSONPath表达(需要Jayway JsonPath表达式)直接绑定到请求参数上
@ProjectedPayload
public interface UserPayload {
@JsonPath("$..firstname")
String getFirstname();
@JsonPath({ "$.lastname", "$.user.lastname" })
String getLastname();
}
上方的投影类型可以被用作Spring MVC方法的参数,上例中Spring MVC会从JSON文档的所有节点下查找firstname
参数、从根节点如果没有找到再去user
子节点下查找lastname
参数,上述内容可以在Jayway JsonPath官方文档中找到
投影的具体内容可以在Spring Data JPA的投影中找到,如果方法返回一个复杂的没有接口来传递的类型,使用Jackson的ObjectMapper
来映射最终值
Querydsl Web支持【跳过】