如何正确使用 Spring Data 中的 PagedResourcesAssembler?

Posted

技术标签:

【中文标题】如何正确使用 Spring Data 中的 PagedResourcesAssembler?【英文标题】:How to correctly use PagedResourcesAssembler from Spring Data? 【发布时间】:2014-02-16 06:09:42 【问题描述】:

我正在使用 Spring 4.0.0.RELEASE、Spring Data Commons 1.7.0.M1、Spring Hateoas 0.8.0.RELEASE

我的资源是一个简单的 POJO:

public class UserResource extends ResourceSupport  ... 

我的资源组装器将 User 对象转换为 UserResource 对象:

@Component
public class UserResourceAssembler extends ResourceAssemblerSupport<User, UserResource>  
    public UserResourceAssembler() 
        super(UserController.class, UserResource.class);
    

    @Override
    public UserResource toResource(User entity) 
        // map User to UserResource
    

在我的 UserController 中,我想从我的服务中检索 Page&lt;User&gt;,然后使用 PagedResourcesAssembler 将其转换为 PagedResources&lt;UserResource&gt;,如下所示:https://***.com/a/16794740/1321564

@RequestMapping(value="", method=RequestMethod.GET)
PagedResources<UserResource> get(@PageableDefault Pageable p, PagedResourcesAssembler assembler) 
    Page<User> u = service.get(p)
    return assembler.toResource(u);

这不会调用UserResourceAssembler,只是返回User 的内容,而不是我的自定义UserResource

返回单个资源有效:

@Autowired
UserResourceAssembler assembler;

@RequestMapping(value="id", method=RequestMethod.GET)
UserResource getById(@PathVariable ObjectId id) throws NotFoundException 
    return assembler.toResource(service.getById(id));

PagedResourcesAssembler 需要一些通用参数,但是我不能使用 T toResource(T),因为我不想将我的 Page&lt;User&gt; 转换为 PagedResources&lt;User&gt;,尤其是因为 User 是 POJO 而不是资源。

所以问题是:它是如何工作的?

编辑:

我的 WebMvcConfigurationSupport:

@Configuration
@ComponentScan
@EnableHypermediaSupport
public class WebMvcConfig extends WebMvcConfigurationSupport 
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) 
        argumentResolvers.add(pageableResolver());
        argumentResolvers.add(sortResolver());
        argumentResolvers.add(pagedResourcesAssemblerArgumentResolver());
    

    @Bean
    public HateoasPageableHandlerMethodArgumentResolver pageableResolver() 
        return new HateoasPageableHandlerMethodArgumentResolver(sortResolver());
    

    @Bean
    public HateoasSortHandlerMethodArgumentResolver sortResolver() 
        return new HateoasSortHandlerMethodArgumentResolver();
    

    @Bean
    public PagedResourcesAssembler<?> pagedResourcesAssembler() 
        return new PagedResourcesAssembler<Object>(pageableResolver(), null);
    

    @Bean
    public PagedResourcesAssemblerArgumentResolver pagedResourcesAssemblerArgumentResolver() 
        return new PagedResourcesAssemblerArgumentResolver(pageableResolver(), null);
    

    /* ... */

解决方案:

@Autowired
UserResourceAssembler assembler;

@RequestMapping(value="", method=RequestMethod.GET)
PagedResources<UserResource> get(@PageableDefault Pageable p, PagedResourcesAssembler pagedAssembler) 
    Page<User> u = service.get(p)
    return pagedAssembler.toResource(u, assembler);

【问题讨论】:

【参考方案1】:

您似乎已经找到了正确的使用方法,但我想在这里详细介绍一些细节,以便其他人也能找到。我在this answer 中详细介绍了PagedResourceAssembler

表示模型

Spring HATEOAS 附带了各种表示模型的基类,可以轻松创建配备链接的表示。提供了三种开箱即用的类:

Resource - 物品资源。有效地包裹一些 DTO 或实体,这些 DTO 或实体捕获一个单个项并通过链接丰富它。 Resources - 集合资源,可以是某些东西的集合,但通常是 Resource 实例的集合。 PagedResources - Resources 的扩展,可捕获额外的分页信息,例如总页数等。

所有这些类都派生自ResourceSupport,这是Link 实例的基本容器。

资源组装器

ResourceAssembler 现在是将域对象或 DTO 转换为此类资源实例的缓解组件。这里重要的部分是,它将 one 源对象转换为 one 目标对象。

因此,PagedResourcesAssembler 将采用 Spring Data Page 实例并将其转换为 PagedResources 实例,方法是评估 Page 并创建必要的 PageMetadata 以及 prevnext导航页面的链接。默认情况下 - 这可能是这里有趣的部分 - 它将使用普通的 SimplePagedResourceAssemblerPRA 的内部类)将页面的各个元素转换为嵌套的 Resource 实例。

为了允许对此进行自定义,PRA 具有额外的 toResource(…) 方法,这些方法采用委托 ResourceAssembler 来处理各个项目。所以你最终会得到这样的结果:

 class UserResource extends ResourceSupport  … 

 class UserResourceAssembler extends ResourceAssemblerSupport<User, UserResource>  … 

客户端代码现在看起来像这样:

 PagedResourcesAssembler<User> parAssembler = … // obtain via DI
 UserResourceAssembler userResourceAssembler = … // obtain via DI

 Page<User> users = userRepository.findAll(new PageRequest(0, 10));

 // Tell PAR to use the user assembler for individual items.
 PagedResources<UserResource> pagedUserResource = parAssembler.toResource(
   users, userResourceAssembler);

展望

从即将发布的 Spring Data Commons 1.7 RC1(和 Spring HATEOAS 0.9 过渡)开始,prevnext 链接将生成为符合 RFC6540 的 URI 模板,以公开在 HandlerMethodArgumentResolvers 中配置的分页请求参数对于PageableSort

您可以通过使用 @EnableSpringDataWebSupport 注释配置类来简化上面显示的配置,这样您就可以摆脱所有显式的 bean 声明。

【讨论】:

嗨,你能告诉我如何在PagedResourcesAssembler 中正确使用这些泛型吗?我只能将其用作原始类型。它说PagedResources&lt;R&gt; toResource(Page&lt;T&gt;, ResourceAssemblerSupport&lt;T, R&gt;)。 Eclipse 不会接受这个:PagedResources&lt;UserResource&gt; p = parAssembler.toResource(userPage, userResourceAssembler). 我在上面的答案中添加了PagedResourcesAssembler 的泛型。如果您提供自定义UserResourceAssemblerResourceAssembler&lt;User, UserResource&gt;,则需要注入PagedResourcesAssembler&lt;User&gt; 以最终创建PagedResources&lt;UserResource&gt;。我刚刚在代码库中添加了一个test case,给出了一个工作示例。 面对同样的问题,在 Spring 4.0.6.RELEASE 和 0.16.0.RELEASE 上,toResource 只接受一个参数,adminResourcePages,并且无法传入自定义的 adminResourceAssembler。这是方法签名: toResource(Page, Link) 我在这里使用了错误的 API 吗? 我现在知道了: Page eventAdminPages = new PageImpl(searchedAdminsEvent.getEventAdmins(), pageable, searchedAdminsEvent.getTotalElements()); PagedResources adminPageResources = pagedResourcesAssembler.toResource(eventAdminPages, adminResourceAssembler); 在这种新的资源分页方式中,重写 toResources 是否还有用处? (注意尾随的's')。干杯。【参考方案2】:

我想将资源列表转换为页面。但是当给它 PagedResourcesAssembler 时,它正在吃掉内部链接。

这将使您的列表分页。

 public class JobExecutionInfoResource extends ResourceSupport 
    private final JobExecutionInfo jobExecution;

    public JobExecutionInfoResource(final JobExecutionInfo jobExecution) 
        this.jobExecution = jobExecution;        
        add(ControllerLinkBuilder.linkTo(methodOn(JobsMonitorController.class).get(jobExecution.getId())).withSelfRel()); // add your own links.          
    

    public JobExecutionInfo getJobExecution() 
        return jobExecution;
    

分页资源提供 ResourceAssembler 告诉分页资源使用它,它什么也不做,只是简单地返回它,因为它已经是一个被传递的资源列表。

    private final PagedResourcesAssembler<JobExecutionInfoResource> jobExecutionInfoResourcePagedResourcesAssembler;
    public static final PageRequest DEFAULT_PAGE_REQUEST = new PageRequest(0, 20);
    public static final ResourceAssembler<JobExecutionInfoResource, JobExecutionInfoResource> SIMPLE_ASSEMBLER = entity -> entity;

@GetMapping("/clientCode/propertyCode/summary")
    public PagedResources<JobExecutionInfoResource> getJobsSummary(@PathVariable String clientCode, @PathVariable String propertyCode,
                                                                   @RequestParam(required = false) String exitStatus,
                                                                   @RequestParam(required = false) String jobName,
                                                                   Pageable pageRequest) 
        List<JobExecutionInfoResource> listOfResources = // your code to generate the list of resource;
        int totalCount = 10// some code to get total count;
        Link selfLink = linkTo(methodOn(JobsMonitorController.class).getJobsSummary(clientCode, propertyCode, exitStatus, jobName, DEFAULT_PAGE_REQUEST)).withSelfRel();
        Page<JobExecutionInfoResource> page = new PageImpl<>(jobExecutions, pageRequest, totalCount);
        return jobExecutionInfoResourcePagedResourcesAssembler.toResource(page, SIMPLE_ASSEMBLER, selfLink);
    

【讨论】:

【参考方案3】:

替代方式

另一种方法是使用 Range HTTP 标头(在RFC 7233 中阅读更多内容)。您可以这样定义 HTTP 标头:

Range: resources=20-41

这意味着,您想要从 20 到 41(包括)获取资源。这种方式允许 API 的消费者接收到精确定义的资源。

这只是另一种方式。范围通常与另一个单位(如字节等)一起使用。

推荐方式

如果您想使用分页功能并拥有真正适用的 API(包括超媒体/HATEOAS),那么我建议您将 Page 和 PageSize 添加到您的 URL。示例:

http://host.loc/articles?Page=1&PageSize=20

然后,您可以在 BaseApiController 中读取此数据并在所有请求中创建一些 QueryFilter 对象:


    var requestHelper = new RequestHelper(Request);

    int page = requestHelper.GetValueFromQueryString<int>("page");
    int pageSize = requestHelper.GetValueFromQueryString<int>("pagesize");

    var filter = new QueryFilter
    
        Page = page != 0 ? page : DefaultPageNumber,
        PageSize = pageSize != 0 ? pageSize : DefaultPageSize
    ;

    return filter;

您的 api 应该返回一些包含项目数量信息的特殊集合。

public class ApiCollection<T>

    public ApiCollection()
    
        Data = new List<T>();
    

    public ApiCollection(int? totalItems, int? totalPages)
    
        Data = new List<T>();
        TotalItems = totalItems;
        TotalPages = totalPages;
    

    public IEnumerable<T> Data  get; set; 

    public int? TotalItems  get; set; 
    public int? TotalPages  get; set; 

你的模型类可以继承一些支持分页的类:

public abstract class ApiEntity

    public List<ApiLink> Links  get; set; 


public class ApiLink

    public ApiLink(string rel, string href)
    
        Rel = rel;
        Href = href;
    

    public string Href  get; set; 

    public string Rel  get; set; 

【讨论】:

你的回答与SpringsPagedResourcesAssembler 无关。模型类不应该继承任何 API 的东西。 ... 关于Range:完全不推荐!生成的 URL 不是幂等的,但 GET 请求应该是。这意味着,我可以使用不同的 Range 标头调用相同的 URL 并获得不同的结果。 --- 你不能复制粘贴 URL 得到相同的结果,因为你总是会丢失Range 信息。 1) 同意,我只是展示了另一种方式... Range 通常用于流式传输 api 2) 同意,我的示例不包含任何数据模型类,只有 API 模型类

以上是关于如何正确使用 Spring Data 中的 PagedResourcesAssembler?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Spring Data 中的 CassandraRepository 为 Cassandra 实现分页的正确方法

如何在 Spring data jpa 中正确使用 findBySomeOtherId 而不是 findById?

如何找到正确的 Spring Data JPA 和 Spring 版本的 jar 文件

spring-data-jpa 如何使用多个数据源? [复制]

Spring Boot数据如何使Query正确管理NumberLong?

使用 Spring Data JPA 的服务层中的 Crud 方法