如何在 Spring MVC 控制器中应用 Spring 数据投影?

How to apply Spring Data projections in a Spring MVC controllers?

是否可以在直接调用数据仓库方法时指定projection?这是存储库代码 - 请注意,我不想通过 REST 公开它,而是希望能够从服务或控制器调用它:

@RepositoryRestResource(exported = false)
public interface UsersRepository extends PagingAndSortingRepository<User, Long> {

    @Query(value = "SELECT u FROM User u WHERE ....")
    public Page<User> findEmployeeUsers(Pageable p);
}

然后在控制器中我这样做:

@PreAuthorize(value = "hasRole('ROLE_ADMIN')")
@RequestMapping(value = "/users/employee")
public Page<User> listEmployees(Pageable pageable) {
    return usersRepository.findEmployeeUsers(pageable);
}

在直接调用findEmployeeUsers方法时,有没有办法指定projectionprojection

我意识到上面的代码对某些人来说可能看起来很奇怪...可以通过 REST 公开存储库并将 @PreAuthorize 东西放入存储库中。思想控制器是进行安全检查的更合适的地方 - 它更自然也更容易测试。

那么,projection 东西能否以某种方式传递到直接调用的存储库方法中?

不,不是,尤其是因为预测通常根据具体情况应用于查询执行的结果。因此,它们目前被设计为有选择地应用于域类型。

截至最新的 Spring Data Fowler 发布序列 GA 版本,可以在 Spring MVC 控制器中以编程方式使用投影基础设施。只需为 SpelAwareProxyProjectionFactory:

声明一个 Spring bean
@Configuration
class SomeConfig {

  @Bean
  public SpelAwareProxyProjectionFactory projectionFactory() {
    return new SpelAwareProxyProjectionFactory();
  }
}

然后将其注入您的控制器并使用它:

@Controller
class SampleController {

  private final ProjectionFactory projectionFactory;

  @Autowired
  public SampleController(ProjectionFactory projectionFactory) {
    this.projectionFactory = projectionFactory;
  }

  @PreAuthorize(value = "hasRole('ROLE_ADMIN')")
  @RequestMapping(value = "/users/employee")
  public Page<?> listEmployees(Pageable pageable) {

    return usersRepository.findEmployeeUsers(pageable).//
      map(user -> projectionFactory.createProjection(Projection.class, user);
  }
}

了解最新版本 Page 如何具有可用于动态转换页面内容的 map(…) 方法。我们使用 JDK 8 lambda 来提供使用 ProjectionFactory.

的转换步骤

除@Oliver 的回答外,如果您想按名称查找投影,如 SpringDataRest 所做的那样(而不是将它们硬连接到你的控制器),这是你必须做的:

  1. RepositoryRestConfiguration 注入您的控制器。该 bean 使您可以访问名为 ProjectionDefinitions 的 class(参见 getProjectionConfiguration()),它充当投影元数据目录。
  2. 使用 ProjectionDefinitions 您可以检索投影 类 给定它们的名称及其关联的边界 classes。
  3. 稍后,您可以使用@Oliver 详述的方法来创建投影实例...

这是一个实现我所描述内容的小型控制器:

@RestController
@RequestMapping("students")
public class StudentController {
    /**
     * {@link StudentController} logger.
     */
    private static final Logger logger =
            LoggerFactory.getLogger(StudentController.class);


    /**
     * Projections Factory.
     */
    private ProjectionFactory p8nFactory;

    /**
     * Projections Directory.
     */
    private ProjectionDefinitions p8nDefs;

    /**
     * {@link Student} repository.
     */
    private StudentRepository repo;

    /**
     * Class Constructor.
     *
     * @param repoConfig
     *      {@code RepositoryRestConfiguration} bean
     * @param p8nFactory
     *      Factory used to create projections
     * @param repo
     *      {@link StudentRepository} instance
     */
    @Autowired
    public StudentController(
        RepositoryRestConfiguration repoConfig, 
        ProjectionFactory p8nFactory,
        StudentRepository repo
    ) {
        super();
        this.p8nFactory = p8nFactory;
        this.p8nDefs    = repoConfig.getProjectionConfiguration();
        this.repo       = repo;
    }
    
    ...
    
    /**
     * Retrieves all persisted students.
     *
     * @param projection
     *      (Optional) Name of the projection to be applied to
     *      students retrieved from the persistence layer
     * @return
     *      {@code ResponseEntity} whose content can be a list of Students
     *      or a projected view of them
     */
    @GetMapping(path = "", produces = APPLICATION_JSON_VALUE)
    public ResponseEntity<Object> retrieveAll(
        @RequestParam(required = false) String projection
    ) {
        Class<?> type;                  // Kind of Projection to be applied
        List<?> rawData;                // Raw Entity Students
        List<?> pjData;                 // Projected students (if applies)

        rawData = this.repo.findAll();
        pjData  = rawData;

        if (projection != null) {
            type   = this.p8nDefs.getProjectionType(Student.class, projection);
            pjData = rawData
                        .stream()
                        .map(s -> this.p8nFactory.createProjection(type, s))
                        .collect(Collectors.toList());
        }
        return new ResponseEntity<>(pjData, HttpStatus.OK);
    }
}

后期可以轻松完成Spring Data Rest发布!

您需要做的就是:

  1. 将投影名称作为请求参数传递

    `/api/users/search/findEmployeeUsers?projection=userView`
    
  2. return PagedModel<PersistentEntityResource> 而不是您的服务方法中的 Page<User>

完成!

我假设您想从您的自定义控制器调用此服务方法,在这种情况下您需要从您的控制器方法 return ResponseEntity<PagedModel<PersistentEntityResource>>

不希望它可分页?只需 return ResponseEntity<CollectionModel<PersistentEntityResource>> 即可。 另请查看 example for single resoure projection.

Spring Data Rest 负责在 api 请求中将 @Projections 应用到 PersistentEntityResources,就像你不断地暴露你的 @RestResource @RepositoryRestResource;相同的投影行为,保持相同的命名约定,基本相同的 URI(对于当前示例)。

您的带有一些业务逻辑的服务方法可能如下所示:

    @Override
    @Transactional(readOnly = true)
    public PagedModel<PersistentEntityResource> listEmployees(Pageable pageable, PersistentEntityResourceAssembler resourceAssembler) {
        Page<User> users = userRepository.findEmployeeUsers(pageable);

        List<User> entities = users.getContent();
        entities.forEach(user -> user.setOnVacation(isUserOnVacationNow(user)));

        CollectionModel<PersistentEntityResource> collectionModel = resourceAssembler.toCollectionModel(entities);

        return PagedModel.of(collectionModel.getContent(), new PagedModel.PageMetadata(
                users.getSize(),
                users.getNumber(),
                users.getTotalElements(),
                users.getTotalPages()));
    }

您的控制器方法可能如下所示:

@BasePathAwareController
public class UsersController {

    @GetMapping(value = "/users/search/findEmployeeUsers")
    ResponseEntity<PagedModel<PersistentEntityResource>> findEmployeeUsers(Pageable pageable,
                                                                    PersistentEntityResourceAssembler resourceAssembler) {
        return ResponseEntity.status(HttpStatus.OK)
                .body(userService.listEmployees(pageable, resourceAssembler));
    }
}

我正在使用 spring-boot-starter-data-rest:2.3.4.RELEASE 和 spring-data-rest-webmvc:3.3.4.RELEASE和 spring-data-rest-webmvc:3.3.4.RELEASE 作为依赖项,将其配置为我的 pom.xml

的父项
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>