2 months ago

有關如何自訂 Page 物件與仿照 SpringDataRest 格式
為什麼要仿照? 主要原因

  • SpringDataRest 提供的是 HATEOAS (Hypermedia As The Engine Of Application State)
  • org.springframework.data.domain.Page 轉成 json 跟 SpringDataRest 不一樣

HATEOAS 的入門說明

自定義回傳 Page 物件

因為 Hibernate 外鍵策略的複雜,在程式中會盡量簡化或不使用

首先說明如果我們要回傳自定義的 DTO 要怎麼做

假如我們有這個資料庫的物件

@Data
@Entity
@Table(name = "project")
@EntityListeners(AuditingEntityListener.class)
public class Project {
    @Id
    @Column(name = "projectid")
    private String projectid;
    @CreatedDate
    @Column(name = "createddate")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
    private Date createddate;
    @CreatedBy
    @Column(name = "createdby")
    private String createdby;
    @LastModifiedDate
    @Column(name = "lastmodifieddate")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss'Z'", timezone = "UTC")
    private Date lastmodifieddate;
    @LastModifiedBy
    @Column(name = "lastmodifiedby")
    private String lastmodifiedby;
}

因為現在外鍵的部分我們必須自己動手轉

@Data
public class ProjectDto {
    private String projectid;
    private Date createddate;
    private String createdby;
    private Date lastmodifieddate;
    private String lastmodifiedby;
    private List<ProjectMember> projectMemberList;
}

我們的 Repository

@RepositoryRestResource
public interface ProjectRepository extends JpaRepository<Project, String> {
    Page<Project> findByProjectidIn(@Param("projectids") List<String> projectids, Pageable pageable);
}

定義一個轉換器

@Component
public class ProjectConverter {

    @Autowired
    private ProjectMemberRepository projectMemberRepository;

    public List<ProjectDto> convert(List<Project> projectList) {
        ModelMapper modelMapper = new ModelMapper();
        List<ProjectDto> projectDtoList = new ArrayList<>();
        projectList.forEach(project -> {
            ProjectDto projectDto = this.convert(project);
            projectDtoList.add(projectDto);
        });
        return projectDtoList;
    }

    public ProjectDto convert(Project project) {
        ModelMapper modelMapper = new ModelMapper();
        ProjectDto projectDto = modelMapper.map(project, ProjectDto.class);
        List<ProjectMember> projectMemberList = projectMemberRepository.findByProjectid(project.getProjectid());
        projectDto.setProjectMemberList(projectMemberList);
        return projectDto;
    }
}

實際上使用

@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "v1/my/projects", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Page<ProjectDto> myProject(
        @PageableDefault(value = 20, sort = {"createddate"}, direction = Sort.Direction.DESC) Pageable pageable){
    Page<Project> projectDtoPage = myService.findMyProject(pageable);
    return new PageImpl<ProjectDto>(projectConverter.convert(projectDtoPage.getContent()), pageable, projectDtoPage.getTotalElements());
}

就可以取得這樣的回傳資料

{
  "content" : [ {
    "projectid" : "bEDHArwqZu",
    "demandcode" : "sam-test",
    "demandname" : "sam-test",
    "demanddesc" : "sam-test",
    "presenter" : "sam-test",
    "priority" : 50,
    "suspended" : false,
    "completed" : false,
    "ended" : false,
    "createddate" : "2017-07-11T01:50:40Z",
    "createdby" : "admin",
    "lastmodifieddate" : "2017-07-11T01:50:40Z",
    "lastmodifiedby" : "admin"
  }, {
    "projectid" : "SHO8OTDAu6",
    "demandcode" : "測試543",
    "demandname" : "測試543",
    "demanddesc" : "測試543",
    "presenter" : "測試543",
    "priority" : 100,
    "suspended" : false,
    "completed" : false,
    "ended" : false,
    "createddate" : "2017-07-10T07:59:34Z",
    "createdby" : "admin",
    "lastmodifieddate" : "2017-07-10T07:59:34Z",
    "lastmodifiedby" : "admin"
  }, {
    "projectid" : "fPlSt22Xn1",
    "demandcode" : "测试001",
    "demandname" : "测试测试",
    "demanddesc" : "",
    "presenter" : "俊雄",
    "priority" : 50,
    "suspended" : false,
    "completed" : false,
    "ended" : false,
    "createddate" : "2017-07-10T05:02:12Z",
    "createdby" : "admin",
    "lastmodifieddate" : "2017-07-10T05:02:12Z",
    "lastmodifiedby" : "admin"
  }, {
    "projectid" : "TNvJCo9T22",
    "demandcode" : "test1",
    "demandname" : "001",
    "demanddesc" : "",
    "presenter" : "俊雄",
    "priority" : 50,
    "suspended" : false,
    "completed" : false,
    "ended" : false,
    "createddate" : "2017-07-10T05:01:50Z",
    "createdby" : "admin",
    "lastmodifieddate" : "2017-07-10T05:01:50Z",
    "lastmodifiedby" : "admin"
  }, {
    "projectid" : "OWWliwK4Rb",
    "demandcode" : "test001",
    "demandname" : "001",
    "demanddesc" : "",
    "presenter" : "俊雄",
    "priority" : 50,
    "suspended" : false,
    "completed" : false,
    "ended" : false,
    "createddate" : "2017-07-10T05:01:18Z",
    "createdby" : "admin",
    "lastmodifieddate" : "2017-07-10T05:01:18Z",
    "lastmodifiedby" : "admin"
  }, {
    "projectid" : "dAP9Ff6F07",
    "demandcode" : "123",
    "demandname" : "123",
    "demanddesc" : "123",
    "presenter" : "123",
    "priority" : 50,
    "suspended" : false,
    "completed" : true,
    "ended" : false,
    "createddate" : "2017-07-03T09:47:36Z",
    "createdby" : "admin",
    "lastmodifieddate" : "2017-07-04T09:08:08Z",
    "lastmodifiedby" : "admin"
  } ],
  "totalPages" : 1,
  "totalElements" : 6,
  "last" : true,
  "number" : 0,
  "size" : 10,
  "numberOfElements" : 6,
  "sort" : [ {
    "direction" : "DESC",
    "property" : "createddate",
    "ignoreCase" : false,
    "nullHandling" : "NATIVE",
    "ascending" : false,
    "descending" : true
  } ],
  "first" : true
}

回傳跟SpringDataRest 一樣的 Page 物件

上面的方式是以前 Spring Data 的共用物件做出來的
但是如果你已經開始用 @RepositoryRestResource 在操作的話你會發現格式上略有差異 如下

{
  "_embedded" : {
    "kpiTargets" : [ {
      "kpino" : "345rr",
      "kpiName" : "34r34r",
      "oncePoint" : 33,
      "maxPoint" : 33,
      "description" : null,
      "state" : 0,
      "actionType" : 0,
      "percentage" : 333.0,
      "teamId" : 4,
      "teamName" : "",
      "targetPoint" : "taskCodeDevelop",
      "createdDate" : "2017-07-06T01:59:12Z",
      "createdBy" : "admin",
      "lastModifiedDate" : "2017-07-06T01:59:12Z",
      "lastModifiedBy" : "admin",
      "_links" : {
        "self" : {
          "href" : "http://localhost:8080/kpiTargets/345rr"
        },
        "kpiTarget" : {
          "href" : "http://localhost:8080/kpiTargets/345rr"
        }
      }
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/kpiTargets{?page,size,sort}",
      "templated" : true
    },
    "profile" : {
      "href" : "http://localhost:8080/profile/kpiTargets"
    }
  },
  "page" : {
    "size" : 20,
    "totalElements" : 1,
    "totalPages" : 1,
    "number" : 0
  }
}

不過上面的格式是 Spring Data Rest 提供的格式,想必要改非常難,那我們就仿照做一樣的提供給前端吧

首先要有 ResourceSupport 的物件

@Data
public class ProjectResource extends ResourceSupport {
    private String projectid;
    private Date createddate;
    private String createdby;
    private Date lastmodifieddate;
    private String lastmodifiedby;
    private List<ProjectMember> projectMemberList;
}

再來配置我們的轉換工具

ProjectResourceAsm.java
import com.ps.controller.resources.ProjectResource;
import com.ps.model.Project;
import com.ps.model.ProjectMember;
import com.ps.repository.ProjectMemberRepository;
import lombok.Data;
import org.modelmapper.ModelMapper;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;

import java.util.List;

/**
 * Created by samchu on 2017/7/11.
 */
@Data
public class ProjectResourceAsm extends ResourceAssemblerSupport<Project, ProjectResource> {

    private ProjectMemberRepository projectMemberRepository;

    /**
     * Creates a new {@link ResourceAssemblerSupport} using the given controller class and resource type.
     *
     * @param controllerClass must not be {@literal null}.
     * @param resourceType    must not be {@literal null}.
     */
    public ProjectResourceAsm(Class<?> controllerClass, Class<ProjectResource> resourceType) {
        super(controllerClass, resourceType);
    }

    @Override
    public ProjectResource toResource(Project entity) {
        ModelMapper modelMapper = new ModelMapper();
        ProjectResource projectResource = modelMapper.map(entity, ProjectResource.class);
        List<ProjectMember> projectMemberList = projectMemberRepository.findByProjectid(entity.getProjectid());
        projectResource.setProjectMemberList(projectMemberList);
        return projectResource;
    }
}

實際上在 Controller 使用

MyRestController.java
@Autowired
private ProjectMemberRepository projectMemberRepository;

@ResponseStatus(HttpStatus.OK)
@GetMapping(value = "v1/my/projects", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public PagedResources<ProjectResource> myProject(
        @PageableDefault(value = 20, sort = {"createddate"}, direction = Sort.Direction.DESC) Pageable pageable) {
    Page<Project> projectDtoPage = myService.findMyProject(pageable);
    HateoasPageableHandlerMethodArgumentResolver resolver = new HateoasPageableHandlerMethodArgumentResolver();
    PagedResourcesAssembler<Project> projectDtoPagedResourcesAssembler = new PagedResourcesAssembler<Project>(resolver, null);
    ProjectResourceAsm projectResourceAsm = new ProjectResourceAsm(MyRestController.class, ProjectResource.class);
    projectResourceAsm.setProjectMemberRepository(projectMemberRepository);
    return projectDtoPagedResourcesAssembler.toResource(projectDtoPage, projectResourceAsm);
}

這樣做出來的元件轉換後的資料格式大致上就跟 SpringDataRest 的一樣了

{
  "_embedded" : {
    "projectResources" : [ {
      "projectid" : "bEDHArwqZu",
      "demandcode" : "sam-test",
      "demandname" : "sam-test",
      "demanddesc" : "sam-test",
      "presenter" : "sam-test",
      "priority" : 50,
      "suspended" : false,
      "completed" : false,
      "ended" : false,
      "createddate" : "2017-07-11T01:50:40Z",
      "createdby" : "admin",
      "lastmodifieddate" : "2017-07-11T01:50:40Z",
      "lastmodifiedby" : "admin",
      "projectMemberList" : [ {
        "projectid" : "bEDHArwqZu",
        "accountid" : "xs7WYNZDUA",
        "username" : "admin",
        "name" : "產品人員",
        "createddate" : "2017-07-11T01:50:41Z",
        "createdby" : "admin",
        "lastmodifieddate" : "2017-07-11T01:50:41Z",
        "lastmodifiedby" : "admin"
      } ]
    }, {
      "projectid" : "SHO8OTDAu6",
      "demandcode" : "測試543",
      "demandname" : "測試543",
      "demanddesc" : "測試543",
      "presenter" : "測試543",
      "priority" : 100,
      "suspended" : false,
      "completed" : false,
      "ended" : false,
      "createddate" : "2017-07-10T07:59:34Z",
      "createdby" : "admin",
      "lastmodifieddate" : "2017-07-10T07:59:34Z",
      "lastmodifiedby" : "admin",
      "projectMemberList" : [ {
        "projectid" : "SHO8OTDAu6",
        "accountid" : "BDyLeICzzM",
        "username" : "443331",
        "name" : "貝吉達",
        "createddate" : "2017-07-10T08:04:32Z",
        "createdby" : "admin",
        "lastmodifieddate" : "2017-07-10T08:04:32Z",
        "lastmodifiedby" : "admin"
      }, {
        "projectid" : "SHO8OTDAu6",
        "accountid" : "xs7WYNZDUA",
        "username" : "admin",
        "name" : "產品人員",
        "createddate" : "2017-07-10T07:59:35Z",
        "createdby" : "admin",
        "lastmodifieddate" : "2017-07-10T07:59:35Z",
        "lastmodifiedby" : "admin"
      } ]
    }, {
      "projectid" : "fPlSt22Xn1",
      "demandcode" : "测试001",
      "demandname" : "测试测试",
      "demanddesc" : "",
      "presenter" : "俊雄",
      "priority" : 50,
      "suspended" : false,
      "completed" : false,
      "ended" : false,
      "createddate" : "2017-07-10T05:02:12Z",
      "createdby" : "admin",
      "lastmodifieddate" : "2017-07-10T05:02:12Z",
      "lastmodifiedby" : "admin",
      "projectMemberList" : [ {
        "projectid" : "fPlSt22Xn1",
        "accountid" : "xs7WYNZDUA",
        "username" : "admin",
        "name" : "產品人員",
        "createddate" : "2017-07-10T05:02:12Z",
        "createdby" : "admin",
        "lastmodifieddate" : "2017-07-10T05:02:12Z",
        "lastmodifiedby" : "admin"
      } ]
    }, {
      "projectid" : "TNvJCo9T22",
      "demandcode" : "test1",
      "demandname" : "001",
      "demanddesc" : "",
      "presenter" : "俊雄",
      "priority" : 50,
      "suspended" : false,
      "completed" : false,
      "ended" : false,
      "createddate" : "2017-07-10T05:01:50Z",
      "createdby" : "admin",
      "lastmodifieddate" : "2017-07-10T05:01:50Z",
      "lastmodifiedby" : "admin",
      "projectMemberList" : [ {
        "projectid" : "TNvJCo9T22",
        "accountid" : "xs7WYNZDUA",
        "username" : "admin",
        "name" : "產品人員",
        "createddate" : "2017-07-10T05:01:50Z",
        "createdby" : "admin",
        "lastmodifieddate" : "2017-07-10T05:01:50Z",
        "lastmodifiedby" : "admin"
      } ]
    }, {
      "projectid" : "OWWliwK4Rb",
      "demandcode" : "test001",
      "demandname" : "001",
      "demanddesc" : "",
      "presenter" : "俊雄",
      "priority" : 50,
      "suspended" : false,
      "completed" : false,
      "ended" : false,
      "createddate" : "2017-07-10T05:01:18Z",
      "createdby" : "admin",
      "lastmodifieddate" : "2017-07-10T05:01:18Z",
      "lastmodifiedby" : "admin",
      "projectMemberList" : [ {
        "projectid" : "OWWliwK4Rb",
        "accountid" : "xs7WYNZDUA",
        "username" : "admin",
        "name" : "產品人員",
        "createddate" : "2017-07-10T05:01:19Z",
        "createdby" : "admin",
        "lastmodifieddate" : "2017-07-10T05:01:19Z",
        "lastmodifiedby" : "admin"
      } ]
    }, {
      "projectid" : "dAP9Ff6F07",
      "demandcode" : "123",
      "demandname" : "123",
      "demanddesc" : "123",
      "presenter" : "123",
      "priority" : 50,
      "suspended" : false,
      "completed" : true,
      "ended" : false,
      "createddate" : "2017-07-03T09:47:36Z",
      "createdby" : "admin",
      "lastmodifieddate" : "2017-07-04T09:08:08Z",
      "lastmodifiedby" : "admin",
      "projectMemberList" : [ {
        "projectid" : "dAP9Ff6F07",
        "accountid" : "BDyLeICzzM",
        "username" : "443331",
        "name" : "貝吉達",
        "createddate" : "2017-07-03T09:48:44Z",
        "createdby" : "admin",
        "lastmodifieddate" : "2017-07-03T09:48:44Z",
        "lastmodifiedby" : "admin"
      }, {
        "projectid" : "dAP9Ff6F07",
        "accountid" : "xs7WYNZDUA",
        "username" : "admin",
        "name" : "產品人員",
        "createddate" : "2017-07-03T09:47:37Z",
        "createdby" : "admin",
        "lastmodifieddate" : "2017-07-03T09:47:37Z",
        "lastmodifiedby" : "admin"
      } ]
    } ]
  },
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/api/v1/my/projects?page=0&size=10&sort=createddate,desc"
    }
  },
  "page" : {
    "size" : 10,
    "totalElements" : 6,
    "totalPages" : 1,
    "number" : 0
  }
}

當然還可以配置很多 link 之類的讓前端可以更簡單操作資料,不過目前這樣就解決了兩種 API 格式不同的問題了

← SpringBootAdmin add Line push message Controller ExceptionHandler →
 
comments powered by Disqus