• Spring Data JPA 之 Web MVC 开发的支持


    15 JPA 对 Web MVC 开发的支持

    我们使⽤ Spring Data JPA 的时候,⼀般都会⽤到 Spring MVC,Spring Data 对 Spring MVC 做了很好的⽀持,体现在以下⼏个⽅⾯:

    1. ⽀持在 Controller 层直接返回实体,⽽不使⽤其显式的调⽤⽅法;
    2. 对 MVC 层⽀持标准的分⻚和排序功能;
    3. 扩展的插件⽀持 Querydsl,可以实现⼀些通⽤的查询逻辑。

    正常情况下,我们开启 Spring Data 对 Spring Web MVC ⽀持的时候需要在 @Configuration 的配置⽂件⾥⾯添加 @EnableSpringDataWebSupport 这⼀注解,如下⾯这种形式:

    @Configuration
    @EnableWebMvc
    // 开启⽀持Spring Data Web的⽀持
    @EnableSpringDataWebSupport
    public class WebConfiguration { }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    由于我们⽤了 Spring Boot,其有⾃动加载机制,会⾃动加载 SpringDataWebAutoConfiguration 类,发⽣如下变化:

    @Configuration(proxyBeanMethods = false)
    @EnableSpringDataWebSupport
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ PageableHandlerMethodArgumentResolver.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(PageableHandlerMethodArgumentResolver.class)
    @EnableConfigurationProperties(SpringDataWebProperties.class)
    @AutoConfigureAfter(RepositoryRestMvcAutoConfiguration.class)
    public class SpringDataWebAutoConfiguration {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    从类上⾯可以看出来,@EnableSpringDataWebSupport 会⾃动开启,所以当我们⽤ Spring Boot + JPA + MVC 的时候,什么都不需要做,因为 Spring Boot 利⽤ Spring Data 对 Spring MVC 做了很多 Web 开发的天然⽀持。⽀持的组件有 DomainConverter、Page、Sort、Databinding、Dynamic Param 等。

    那么我们先来看⼀下它对 DomainClassConverter 组件的⽀持。

    15.1 DomainClassConverter 组件

    这个组件的主要作⽤是帮我们把 Path 中 ID 的变量,或 Request 参数中的变量 ID 的参数值,直接转化成实体对象注册到 Controller ⽅法的参数⾥⾯。怎么理解呢?我们看个例⼦,就很好懂了

    15.1.1 一个实例

    ⾸先,写⼀个 MVC 的 Controller,分别从 Path 和 Param 变量⾥⾯,根据 ID 转化成实体,代码如下:

    @RestController
    public class UserController {
    
        /**
         * 从 path 变量⾥⾯获得参数 ID 的值,然后直接转化成 user 实体
         */
        @GetMapping("/user/{id}")
        public User getUserFromPath(@PathVariable("id") User user) {
            return user;
        }
    
        /**
         * 将 request 的 param 中的 ID 变量值,转化成 user 实体
         */
        @GetMapping("/user")
        public User getUserFromRequestParam(@RequestParam("id") User user) {
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    然后,我们运⾏起来,看⼀下结果:

    ### 根据 id 获取用户信息
    GET http://127.0.0.1:8080/user/1
    
    ### 根据 id 获取用户信息
    GET http://127.0.0.1:8080/user?id=1
    
    • 1
    • 2
    • 3
    • 4
    • 5

    运行结果如下:

    HTTP/1.1 200 
    Content-Type: application/json
    
    {
      "id": 1,
      "version": 0,
      "deleted": false,
      "createUserId": 245273790,
      "createdDate": "2022-07-31T08:32:38.915",
      "lastModifiedUserId": 245273790,
      "lastModifiedDate": "2022-07-31T08:32:38.915",
      "name": "zzn",
      "email": "973536793@qq.com",
      "sex": "BOY",
      "age": 18
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    从结果来看,Controller ⾥⾯的 getUserFromRequestParam ⽅法会⾃动根据 ID 查询实体对象 User,然后注⼊⽅法的参数⾥⾯。那它是怎么实现的呢?我们看⼀下源码。

    15.1.2 源码分析

    我们打开 DomainClassConverter 类,⾥⾯有个 ToEntityConverter 的内部转化类的 Matches ⽅法,它会判断参数的类型是不是实体,并且有没有对应的实体 Repositorie 存在。如果不存在,就会直接报错说找不到合适的参数转化器。

    DomainClassConverter ⾥⾯的关键代码如下:

    public class DomainClassConverter<T extends ConversionService & ConverterRegistry>
        implements ConditionalGenericConverter, ApplicationContextAware {
        
        @Nullable
    		@Override
    		public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
    
    			if (source == null || !StringUtils.hasText(source.toString())) {
    				return null;
    			}
    
    			if (sourceType.equals(targetType)) {
    				return source;
    			}
    
    			Class<?> domainType = targetType.getType();
    			RepositoryInvoker invoker = repositoryInvokerFactory.getInvokerFor(domainType);
    			RepositoryInformation information = repositories.getRequiredRepositoryInformation(domainType);
    
    			Object id = conversionService.convert(source, information.getIdType());
                // 调用 findById 执行查询
    			return id == null ? null : invoker.invokeFindById(id).orElse(null);
    		}
    
        @Override
        public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
            // 判断参数的类型是不是实体
            if (sourceType.isAssignableTo(targetType)) {
                return false;
            }
    
            Class<?> domainType = targetType.getType();
            // 有没有对应的实体的 Repository 存在
            if (!repositories.hasRepositoryFor(domainType)) {
                return false;
            }
    
            Optional<RepositoryInformation> repositoryInformation = repositories.getRepositoryInformationFor(domainType);
    
            return repositoryInformation.map(it -> {
    
                Class<?> rawIdType = it.getIdType();
    
                return sourceType.equals(TypeDescriptor.valueOf(rawIdType))
                    || conversionService.canConvert(sourceType.getType(), rawIdType);
            }).orElseThrow(
                () -> new IllegalStateException(String.format("Couldn't find RepositoryInformation for %s!", domainType)));
        }
    
    }		
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    所以,我们上⾯的例⼦其实是需要有 UserInfoRepository 的,否则会失败。通过源码我们也可以看到,如果 matches=true,那么就会执⾏ convert ⽅法,最终调⽤ findById 的⽅法帮我们执⾏查询动作。

    SpringDataWebConfiguration 因为实现了 WebMvcConfigurer 的 addFormatters 所有加载了⾃定义参数转化器的功能,所以才有了 DomainClassConverter 组件的⽀持。关键代码如下:

    @Configuration(proxyBeanMethods = false)
    public class SpringDataWebConfiguration implements WebMvcConfigurer, BeanClassLoaderAware {
        
        @Override
    	public void addFormatters(FormatterRegistry registry) {
    
    		registry.addFormatter(DistanceFormatter.INSTANCE);
    		registry.addFormatter(PointFormatter.INSTANCE);
    
    		if (!(registry instanceof FormattingConversionService)) {
    			return;
    		}
    
    		FormattingConversionService conversionService = (FormattingConversionService) registry;
    
    		DomainClassConverter<FormattingConversionService> converter = new DomainClassConverter<>(conversionService);
    		converter.setApplicationContext(context);
    	}
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    从源码上我们也可以看到,DomainClassConverter 只会根据 ID 来查询实体,很有局限性,没有更加灵活的参数转化功能,不过你也可以根据源码⾃⼰进⾏扩展,我在这就不展示更多了。

    下⾯来看⼀下JPA 对 Web MVC 分⻚和排序是如何⽀持的。

    15.2 Page 和 Sort 的参数支持

    15.2.1 一个实例

    在我们之前的 UserController ⾥⾯添加如下两个⽅法,分别测试分⻚和排序。

    @GetMapping("/users")
        public Page<User> queryByPage(Pageable pageable, User user) {
            return userRepository.findAll(Example.of(user), pageable);
        }
    
        @GetMapping("/users/sort")
        public HttpEntity<List<User>> queryBySort(Sort sort) {
            return new HttpEntity<>(userRepository.findAll(sort));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中,queryByPage ⽅法中,两个参数可以分别接收分⻚参数和查询条件,我们请求⼀下,看看效果:

    ### 分页查询用户信息
    GET http://127.0.0.1:8080/users?size=2&page=0&ages=18&sort=id,desc
    
    • 1
    • 2

    参数⾥⾯可以⽀持分⻚⼤⼩为 2、⻚码 0、排序(按照 ID 倒序)、参数 ages=18 的所有结果,如下所示:

    {
      "content": [
        {
          "id": 5,
          "version": 0,
          "sex": "BOY",
          "age": 18
        },
        {
          "id": 4,
          "version": 0,
          "sex": "BOY",
          "age": 18
        }
      ],
      "pageable": {
        "sort": {
          "unsorted": false,
          "sorted": true,
          "empty": false
        },
        "pageNumber": 0,
        "pageSize": 2,
        "offset": 0,
        "unpaged": false,
        "paged": true
      },
      "totalPages": 3,
      "totalElements": 5,
      "last": false,
      "numberOfElements": 2,
      "first": true,
      "sort": {
        "unsorted": false,
        "sorted": true,
        "empty": false
      },
      "size": 2,
      "number": 0,
      "empty": false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    上⾯的字段我就不⼀⼀介绍了,在第 4 课时(如何利⽤ Repository 中的⽅法返回值解决实际问题)我们已经讲过了,只不过现在应⽤到了 MVC 的 View 层。因此,我们可以得出结论:Pageable 既⽀持分⻚参数,也⽀持排序参数。并且从下⾯这⾏代码可以看出其也可以单独调⽤ Sort 参数。

    ### 排序查询
    GET http://127.0.0.1:8080/users/sort?ages=18&sort=id,desc
    
    • 1
    • 2

    那么它的实现原理是什么呢?

    15.2.2 原理分析

    和 DomainClassConverter 组件的⽀持是⼀样的,由于 SpringDataWebConfiguration 实现了 WebMvcConfigurer 接⼝,通过 addArgumentResolvers ⽅法,扩展了 Controller ⽅法的参数 HandlerMethodArgumentResolver 的解决者,从下⾯图⽚中你就可以看出来。

    在这里插入图片描述

    我们可以进去 SortHandlerMethodArgumentResolver 里面看一下源码:

    在这里插入图片描述

    这个类⾥⾯最关键的就是下⾯两个⽅法:

    1. supportsParameter,表示只处理类型为 Sort.class 的参数;
    2. resolveArgument,可以把请求⾥⾯参数的值,转换成该⽅法⾥⾯的参数 Sort 对象。

    这⾥还要提到的是另外⼀个类:PageHandlerMethodArgumentResolver 类。

    在这里插入图片描述

    这个类⾥⾯也有两个最关键的⽅法:

    1. supportsParameter,表示我只处理类型是 Pageable.class 的参数;
    2. resolveArgument,把请求⾥⾯参数的值,转换成该⽅法⾥⾯的参数 Pageable 的实现类 PageRequest。

    关于 Web 请求的分⻚和排序的⽀持就介绍到这⾥,那么如果返回的是⼀个 Projection 的接⼝,Spring 是怎么处理的呢?我们接着看。

    15.3 Web MVC 的参数绑定

    之前我们在讲 Projection 的时候提到过接⼝,Spring Data JPA ⾥⾯,也可以通过 @ProjectedPayload 和 @JsonPath 对接⼝进⾏注解⽀持,不过要注意这与前⾯所讲的 Jackson 注解的区别在于,此时我们讲的是接⼝。

    15.3.1 一个实例

    这⾥我依然结合⼀个实例来对这个接⼝进⾏讲解,请看下⾯的步骤。

    第⼀步:如果要⽀持 Projection,必须要在 maven ⾥⾯引⼊ jsonpath 依赖才可以:

    <dependency>
        <groupId>com.jayway.jsonpathgroupId>
        <artifactId>json-pathartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    第⼆步:新建⼀个 UserForm 接⼝类,⽤来接收接⼝传递的 json 对象。

    @ProjectedPayload
    public interface UserForm {
    
        /**
         * 第⼀级参数 JSON ⾥⾯找 age 字段
         * // @JsonPath("$..age") $.. 代表任意层级找 age 字段
         */
        @JsonPath("$.age")
        Integer getAge();
    
        /**
         * 第⼀级找参数 JSON ⾥⾯的 telephone 字段
         * // @JsonPath({ "$.telephone", "$.user.telephone" })
         * 第⼀级或者 user 下⾯的 telephone 都可以
         */
        @JsonPath("$.telephone")
        String getTelephone();
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    第三步:在 Controller ⾥⾯新建⼀个 post ⽅法,通过接⼝获得 RequestBody 参数对象⾥⾯的值。

    @PostMapping("/users/projected")
    public UserForm saveUserInfo(@RequestBody UserForm form) {
        return form;
    }
    
    • 1
    • 2
    • 3
    • 4

    第四步:我们发送⼀个 post 请求,代码如下:

    POST http://localhost:8080/users/projected
    Content-Type: application/json
    
    {
      "age": 18,
      "telephone": "12345678"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    此时可以正常得到如下结果:

    {
      "telephone": "12345678",
      "age": 18
    }
    
    • 1
    • 2
    • 3
    • 4

    这个响应结果说明了接⼝可以正常映射。现在你知道⽤法了,我们再通过源码分析⼀下其原理。

    15.3.2 原理分析

    很简单,我们还是直接看 SpringDataWebConfiguration,其中实现的 WebMvcConfigurer 接⼝⾥⾯有个 extendMessageConverters ⽅法,⽅法中加了⼀个 ProjectingJackson2HttpMessageConverter 的类,这个类会把带 ProjectedPayload.class 注解的接⼝进⾏ Converter。

    我们看⼀下其中主要的两个⽅法:

    1. 加载 ProjectingJackson2HttpMessageConverter,⽤来做 Projecting 的接⼝转化。我们通过源码看⼀下是在哪⾥被加载进去的,如下:

      在这里插入图片描述

    2. ⽽ ProjectingJackson2HttpMessageConverter 主要是继承了 MappingJackson2HttpMessageConverter,并且实现了 HttpMessageConverter 的接⼝⾥⾯的两个重要⽅法,如下所示:

      public class ProjectingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter
          implements BeanClassLoaderAware, BeanFactoryAware {
          
          
          @Override
          public boolean canRead(Type type, @Nullable Class<?> contextClass, @Nullable MediaType mediaType) {
      
              if (!canRead(mediaType)) {
                  return false;
              }
      
              ResolvableType owner = contextClass == null ? null : ResolvableType.forClass(contextClass);
              Class<?> rawType = ResolvableType.forType(type, owner).resolve(Object.class);
              Boolean result = supportedTypesCache.get(rawType);
      
              if (result != null) {
                  return result;
              }
      
              result = rawType.isInterface() && AnnotationUtils.findAnnotation(rawType, ProjectedPayload.class) != null;
              supportedTypesCache.put(rawType, result);
      
              return result;
          }
      
          @Override
          public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
              throws IOException, HttpMessageNotReadableException {
              return projectionFactory.createProjection(ResolvableType.forType(type).resolve(Object.class),
                                                        inputMessage.getBody());
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • canRead 通过判断参数的实体类型⾥⾯是否有接⼝,以及是否有 ProjectedPayload.class 注解后,才进⾏解析;
      • read ⽅法负责把 HttpInputMessage 转化成 Projected 的映射代理对象

    现在你知道了 Spring ⾥⾯是如何通过 HttpMessageConverter 对 Projected 进⾏的⽀持,在使⽤过程中,希望你针对实际情况多去 Debug。不过这个不常⽤,你知道⼀下就可以了。

    下⾯介绍⼀个通过 QueryDSL 对 Web 请求进⾏动态参数查询的⽅法。

    15.4 Querydsl 的 Web MVC 支持

    实际⼯作中,经常有⼈会⽤ Querydsl 做⼀些复杂查询,⽅便⽣成 Rest 的 API 接⼝,那么这种⽅法有什么好处,⼜会暴露什么缺点呢?我们先看⼀个实例。

    15.4.1 一个实例

    这是⼀个通过 QueryDSL 作为请求参数的使⽤案例,通过它你就可以体验⼀下 QueryDSL 的⽤法和使⽤场景,我们⼀步⼀步来看⼀下。

    第⼀步:需要 maven 引⼊ querydsl 的依赖。

    <dependency>
        <groupId>com.querydslgroupId>
        <artifactId>querydsl-aptartifactId>
    dependency>
    
    <dependency>
        <groupId>com.querydslgroupId>
        <artifactId>querydsl-jpaartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    第⼆步:UserRepository 继承 QuerydslPredicateExecutor 接⼝,就可以实现 QueryDSL 的查询⽅法了,代码如下:

    public interface UserRepository extends BaseRepository<User, Long>, QuerydslPredicateExecutor<User> {}
    
    • 1

    第三步:Controller ⾥⾯直接利⽤ @QuerydslPredicate 注解接收 Predicate predicate 参数。

    @GetMapping(value = "user/dsl")
    public Page<User> queryByDsl(@QuerydslPredicate(root = User.class) Predicate predicate, Pageable pageable) {
        // 这⾥⾯我⽤的 userRepository ⾥⾯的 QuerydslPredicateExecutor ⾥⾯的⽅法
        return userRepository.findAll(predicate, pageable);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第四步:直接请求我们的 user/dsl 即可,这⾥利⽤ queryDsl 的语法 ,使 &ages=18 作为我们的请求参数。

    {
        "content": [
            {
                "id": 5,
                "version": 0,
                "sex": "BOY",
                "age": 18
            },
            {
                "id": 4,
                "version": 0,
                "sex": "BOY",
                "age": 18
            }
        ],
        "pageable": {
            "sort": {
                "sorted": true,
                "unsorted": false,
                "empty": false
            },
            "pageNumber": 0,
            "pageSize": 2,
            "offset": 0,
            "paged": true,
            "unpaged": false
        },
        "last": false,
        "totalPages": 3,
        "totalElements": 5,
        "first": true,
        "numberOfElements": 2,
        "size": 2,
        "number": 0,
        "sort": {
            "sorted": true,
            "unsorted": false,
            "empty": false
        },
        "empty": false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    现在我们可以得出结论:QuerysDSL 可以帮我们省去创建 Predicate 的过程,简化了操作流程。但是它依然存在⼀些局限性,⽐如多了⼀些模糊查询、范围查询、⼤⼩查询,它对这些⽅⾯的⽀持不是特别友好。可能未来会更新、优化,不过在这⾥你只要关注⼀下就可以了。

    15.4.2 原理分析

    QueryDSL 也是主要利⽤⾃定义 Spring MVC 的 HandlerMethodArgumentResolver 实现类,根据请求的参数字段,转化成 Controller ⾥⾯所需要的参数,请看⼀下源码。

    public class QuerydslPredicateArgumentResolver extends QuerydslPredicateArgumentResolverSupport
          implements HandlerMethodArgumentResolver {
        
        @Nullable
    	@Override
    	public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
    			NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    
    		MultiValueMap<String, String> queryParameters = getQueryParameters(webRequest);
    		Predicate result = getPredicate(parameter, queryParameters);
    
    		return potentiallyConvertMethodParameterValue(parameter, result);
    	}
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在实际开发中,关于 insert 和 update 的接⼝我们是“逃不掉”的,但不是每次的字段都会全部传递过来,那这个时候我们应该怎么做呢?这就涉及了上述实例⾥⾯的两个注解 @DynamicUpdate 和 @DynamicInsert,下⾯来详细介绍⼀下。

    15.5 @DynamicUpdate 和 @DynamicInsert 详解

    15.5.1 通过语法快速了解

    @DynamicInsert:这个注解表示 insert 的时候,会动态⽣产 insert SQL 语句,其⽣成 SQL

    的规则是:只有⾮空的字段才能⽣成 SQL。代码如下:

    @Target(TYPE)
    @Retention(RUNTIME)
    public @interface DynamicInsert {
    
        /**
         * 默认是 true,如果设置成 false,就表示空的字段也会⽣成 sql 语句;
         */
        boolean value() default true;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这个注解主要是⽤在 @Entity 的实体中,如果加上这个注解,就表示⽣成的 insert SQL 的 Columns 只包含⾮空的字段;如果实体中不加这个注解,默认的情况是空的,字段也会作为 insert 语句⾥⾯的 Columns。

    @DynamicUpdate:和 insert 是⼀个意思,只不过这个注解指的是在 update 的时候,会动态产⽣ update SQL 语句,⽣成 SQL 的规则是:只有更新的字段才会⽣成到 update SQL 的 Columns ⾥⾯。 请看代码:

    @Target( TYPE )
    @Retention( RUNTIME )
    public @interface DynamicUpdate {
    
        /**
         * 默认 true,如果设置成 false 和不添加这个注解的效果⼀样
         */
        boolean value() default true; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    和上⼀个注解的原理类似,这个注解也是⽤在 @Entity 的实体中,如果加上这个注解,就表示⽣成的 update SQL 的 Columns 只包含改变的字段;如果不加这个注解,默认的情况是所有的字段也会作为 update 语句⾥⾯的 Columns。

    这样做的⽬的是提⾼ sql 的执⾏效率,默认更新所有字段,这样会导致⼀些到索引的字段也会更新,这样 sql 的执⾏效率就⽐较低了。需要注意的是:这种⽣效的前提是 select before-update 的触发机制。

    15.5.2 使用案例

    第⼀步:为了⽅便测试,我们修改⼀下 User 实体:加上 @DynamicInsert 和 @DynamicUpdate 注解。

    @Entity
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @ToString(callSuper = true)
    @DynamicInsert
    @DynamicUpdate
    public class User extends BaseEntity {
    
        private String name;
        private String email;
        @Enumerated(EnumType.STRING)
        private SexEnum sex;
        private Integer age;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    第二步:准备测试用例

    @Slf4j
    @DataJpaTest
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    @Import(JpaConfiguration.class)
    class UserRepositoryTest {
    
        @Test
        void test_dynamic_insert() {
            User user = User.builder().name("zzn").email("123456@126.com").build();
            userRepository.saveAndFlush(user);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    第三步:执行测试用例,查看日志输出

    Hibernate: insert into user (create_user_id, created_date, deleted, last_modified_date, last_modified_user_id, version, email, name, id) values (?, ?, ?, ?, ?, ?, ?, ?, ?)
    
    • 1

    这时你会发现,除了 BaseEntity ⾥⾯的⼀些基础字段,⽽其他字段并没有⽣成到 insert 语句⾥⾯。

    第四步:准备更新的测试用例

    @Test
    void test_dynamic_update() {
        User user = User.builder().name("zzn").email("123456@126.com").build();
        userRepository.saveAndFlush(user);
        User entity = userRepository.getById(user.getId());
        entity.setName("test_zzn");
        entity.setEmail(null);
        userRepository.saveAndFlush(entity);
        System.out.println(userRepository.findById(user.getId()));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    第五步:执行测试用例,查看日志输出

    Hibernate: update user set last_modified_date=?, last_modified_user_id=?, version=?, email=?, name=? where id=? and version=?
    
    • 1

    通过 SQL 可以看到,只更新了我们有进行变跟的字段,⽽包括了 null 的字段也更新了,如 email 字段中我们传递的是 null。

    通过上⾯的两个例⼦你应该能弄清楚 @DynamicInsert 和 @DynamicUpdate 注解是做什么的了,我们在写 API 的时候就要考虑⼀下是否需要对 null 的字段进⾏操作,因为 JPA 是不知道字段为 null 的时候,是想更新还是不想更新,所以默认 JPA 会⽐较实例对象⾥⾯的所有包括 null 的字段,发现有变化也会更新。

    15.6 本章小结

    通过上⾯的讲解,你会发现 Spring Data 为我们做了不少⽀持 MVC 的⼯作,帮助我们提升了很多开发效率;并且通过原理分析,你也知道了⾃定义 HttpMessageConverter 和 HandlerMethodArgumentResolver 的⽅法。

  • 相关阅读:
    金仓数据库 KingbaseES 插件参考手册(25. dict_xsyn)
    ESP8266 做简单的仪器
    蓝牙技术|上半年全国新增 130 万台充电桩,蓝牙充电桩将成为市场主流
    Python 读取 Word 详解(python-docx)
    关于地方美食的HTML网页设计——地方美食介绍网站 HTML顺德美食介绍 html网页制作代码大全
    DLL详解
    【Spring(四)】Spring基于注解的配置方式
    294_C++_报警状态bit与(&)上通道bit,然后检测置位的通道,得到对应置位通道的告警信息,适用于多通道告警,组成string字符串发送
    flask 实践
    tp5访问的时候必须加index.php,TP5配置隐藏入口index.php文件
  • 原文地址:https://blog.csdn.net/qq_40161813/article/details/126292313