使用链接对象正确解组 Angular FormGroup POST

Properly de-marshall Angular FormGroup POST with linked objects

TL;DR;

在 Angular 2.x + Spring Data REST + Spring Boot 1.4 应用程序中,如何定义 JSON 对象Spring 可以解组 到域模型中的引用?

详情

例如,如果食谱书包含同时创建的 labelsrecipes,并且每个 recipe 都引用一个或多个 labels,则代表可能看起来像:

book: {
  title: 'Cook book',
  labels:[{            <--- array index used in labelRef below
    name: 'fruit',
    description: 'Recipe with fruit.'
  },{
    name: 'vegetable',
    description: 'Recipe with vegetables.'
  },{
    name: 'fish',
    description: 'Recipe with fish.'
  }],
  recipes:[{
    title: 'Sweet corn and onion salad',
    description: 'Simple, quick, and refreshing corn salad',
    labels: [{
      labelRef: 1      <--- using array index as reference
    }]
  }]
}

其中labelRef的数组使用labels中的数组索引作为客户端标签标识符(需要保证数组顺序...)。由于 labelsrecipes 同时发布,因此 labelrecipe 都没有永久标识符。食谱书的 Angular 表单是动态构建的,因此用户可以根据需要添加任意数量的标签和食谱:

    let book =     this._fb.group({
      title:       this._fb.control(''),
      labels:      this._fb.array([this.buildLabel()]),
      recipes:     this._fb.array([this.buildRecipe()])
    });

  buildLabel(): FormGroup {
    return         this._fb.group({
      name:        this._fb.control(''),
      description: this._fb.control('')
    });
  }

  buildRecipe(): FormGroup {
    return         this._fb.group({
      title:       this._fb.control(''),
      description: this._fb.control(''),
      labels:      this._fb.array([])
    });
  }

  buildLabelReference(index: number): FormGroup {
    return         this._fb.group({
      labelIndex:  this._fb.control(index)
    });
  }

SpringData JPA + Lombok中的中间层表示Label:

@Data
@Entity
@Table(name = "LABEL")
public class Label {    
    @Id
    @Column(nullable = false)
    private Long id;

    private String name = "";
    private String description = "";

    @ManyToOne(optional=false)
    @JoinColumn(name="book_id")
    private Book book;
}

Recipe:

@Data
@Entity
@Table(name = "RECIPE")
public class Recipe {
    @Id
    @Column(nullable = false)
    private Long id;

    private String title = "";
    private String description = "";

    @OneToMany(cascade={CascadeType.ALL}, mappedBy="book", orphanRemoval=true)
    @Column(nullable = false)
    private List<Label> labels = new ArrayList<>();

    @ManyToOne(optional=false)
    @JoinColumn(name="book_id")
    private Book book;
}

Book:

@Data
@Entity
@Table(name = "BOOK")
public class Book {
    @Id
    @Column(nullable = false)
    private Long id;

    private String title = "";

    @OneToMany(cascade={CascadeType.ALL}, mappedBy="book", orphanRemoval=true)
    @Column(nullable = false)
    private Set<Label> labels = new HashSet<>();

    @OneToMany(cascade={CascadeType.ALL}, mappedBy="book", orphanRemoval=true)
    @Column(nullable = false)
    private List<Recipe> recipes = new ArrayList<>();

}

Book@RepositoryRestController 中正确解编:

@Slf4j
@RepositoryRestController
public class BookRespositoryRestController {

    private final BookRepository bookRepository;
    private final LabelRepository labelRepository;

    @RequestMapping(method = RequestMethod.POST,value = "/books")
    public @ResponseBody PersistentEntityResource post(@RequestBody Book book,
            PersistentEntityResourceAssembler assembler) {

        Book entity = processPost(book);

        return assembler.toResource(entity);
    }

配方标签除外(因为领域模型不知道labelRef)。

那么,我如何在我的 Angular TypeScript 应用程序中构造一个合适的 labelRef,以便 Book 也解组每个 [=] 中的 label 链接20=] 对象?

TL;DR;

不幸的是,它(目前)还不受支持。但是您可以使用一些自定义代码来添加支持。

详情

有两个基本问题需要解决:定义一个JSON对象引用;并且,编码 client-side JSON 对象引用并解码 server-side JSON 对象引用。

对 JSON 个对象的引用

有很多关于 "standard" JSON 参考的提议(例如参见“Standard way of referencing an object by identity (for, eg, circular references)?”),但截至 2017 年没有 "standard"。

多个请求

如果可以拆分请求,请先提交引用的对象,然后使用唯一的相对 server-side 路径作为对对象的引用:

  /labels/1: {
    name: 'fruit',
    description: 'Recipe with fruit.'
  }

  /labels/2: {
    name: 'vegetable',
    description: 'Recipe with vegetables.'
  }

  /labels/3: {
    name: 'fish',
    description: 'Recipe with fish.'
  } 

然后第二个请求使用 server-side 路径:

  recipes:[{
    title: 'Sweet corn and onion salad',
    description: 'Simple, quick, and refreshing corn salad',
    labels: [{
      label: '/labels/1'
    }]
  }]

Spring Data REST 无需任何自定义代码即可支持此模型(尽管在更复杂的模型中您可能需要 patch-up 任何 bi-directional 指针)。

单个请求

如果您需要使用单个请求,请选择 JSON 对象引用模型并对您的引用进行相应编码。 server-side、de-marshall/de-serialize数据使用自定义代码。

Spring 数据 REST 支持 adding custom (de)serializers to Jackson's ObjectMapper。或者,数据传输对象可以执行 de-marhalling.

我使用了JSON路径并在服务器端向数据传输对象添加了de-serializer:

book: {
  title: 'Cook book',
  labels:[{
    name: 'fruit',
    description: 'Recipe with fruit.'
  },{
    name: 'vegetable',
    description: 'Recipe with vegetables.'
  },{
    name: 'fish',
    description: 'Recipe with fish.'
  }],
  recipes:[{
    title: 'Sweet corn and onion salad',
    description: 'Simple, quick, and refreshing corn salad',
    labels: [{
      label: '$..labels[1]'      <--- using JSONPath reference
    }]
  }]
}