iT邦幫忙

2018 iT 邦幫忙鐵人賽
1
Software Development

30天從零開始 使用 Spring Boot 跟 Spring Cloud 建構完整微服務架構系列 第 35

Day 35 - 讓 Page 物件像 Spring Data 一樣支援 HATEOAS

好了, 用了潮潮的 Spring Data Rest 有 HATEOAS 這樣方便的格式
原本我們使用 Page 物件來回覆前端的 的格式就開始被前端嫌

沒 HATEOAS 好不方便啊, 有兩種不同格式 API 很麻煩耶...等

好吧....拿出後端的尊嚴....

我們來改成跟 Spring Data Rest 提供的 HATEOAS 一樣的效果吧

  • 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;
}

再來配置我們的轉換工具

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 使用

    @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 格式不同的問題了


上一篇
Day 34 - Define entity resource link in spring data rest
系列文
30天從零開始 使用 Spring Boot 跟 Spring Cloud 建構完整微服務架構35
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言