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

25

当直接调用数据存储库方法时,是否可以指定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方法时,是否有办法指定projection

我意识到上面的代码可能看起来很奇怪...可以通过REST公开存储库并将@PreAuthorize标签放在存储库中。思考控制器是执行安全性检查的更恰当地方 - 这更自然,也更容易测试。

因此,是否有办法将projection传递到直接调用的存储库方法中?

3个回答

61

不是这样的,特别是因为投影通常是根据情况对查询执行结果进行选择性应用的。因此,它们当前被设计为有选择地应用于域类型。

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

@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


谢谢,看起来这是一个不错的方法!不幸的是,我无法测试它,因为我被限制在SpringBoot 1.2.2(3)上 - 它没有最新的spring-data项目GA版本...我意识到这是一个不同的问题 - 但有没有办法将GA放入SpringBoot 1.2.2或1.2.3应用程序中以测试这个东西? - Barbadoss
1
以下是如何操作的方法 - http://spring.io/blog/2015/03/26/what-s-new-in-spring-data-fowler,我将尝试并测试新的投影基础设施。 - Barbadoss
@OliverGierke,看起来在当前版本的Spring Data中这个不再起作用了?使用提供的示例,Jackson会抛出一个com.fasterxml.jackson.core.JsonGenerationException: Can not start an object, expecting field name异常。 - adam p
4
将仓库查询结果转换如上例与使用返回投影的仓库查询相比,性能等方面会有怎样的差异? - chrismarx
@OliverGierke,这是我能够遵循的最后一个改进吗?还是您有关于此的一些新的改进? - claudioivp
太好了!现在,有没有办法从@Projection注释中按name属性查找投影?在默认的JpaRepository实现中,这个查找是如何工作的?这个评论是否值得单独提出一个问题? - Michal

4

除了@Oliver的答案之外,如果你想像SpringDataRest一样按名称查找投影(而不是在控制器中硬编码它们),这就是你需要做的:

  1. RepositoryRestConfiguration注入到你的控制器中。这个bean让你可以访问一个名为ProjectionDefinitions的类(参见getProjectionConfiguration()),该类充当投影元数据目录。
  2. 使用ProjectionDefinitions,你可以检索与它们的名称和关联绑定类相关的投影类。
  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);
    }
}

2
在最新的Spring Data Rest版本中,这可以很容易地完成!
你只需要做以下几步:
  1. pass projection name as request param

    `/api/users/search/findEmployeeUsers?projection=userView`
    
  2. return PagedModel<PersistentEntityResource> instead of Page<User> from your service method;

完成!

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

不想使用分页?只需返回 ResponseEntity<CollectionModel<PersistentEntityResource>>。 还可以查看单一资源投影示例

Spring Data Rest会在API请求中应用@ProjectionPersistentEntityResource上,就像您保持从@RepositoryRestResource暴露@RestResource一样;对于投影,保持相同的命名约定,基本上相同的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的父级。请注意保留HTML标签。
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接