Summary
In a complex Angular 2.x + Spring Data REST + Spring Boot 1.4 project, the challenge lies in defining JSON object references that can be successfully de-marshaled by Spring into the domain model.
Key Points
Consider a scenario where a recipe book contains both labels
and recipes
, all created simultaneously. Each recipe
has connections to one or more labels
, leading to a representation like:
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: [{
labelRef: 1
}]
}]
}
The challenge here is using array indices as client-side label identifiers when no persistent IDs exist for either label
or recipe
. This requires maintaining a guaranteed order in the array of labels
.
To tackle this dynamically in an Angular application, a form is implemented allowing users to add labels and recipes at will:
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)
});
}
Moving on to the mid-tier representation in Spring Data JPA + Lombok, we have 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;
}
And then comes 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;
}
Finally, there's 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<>();
}
While the Book
de-marshalls correctly in the @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);
}
the stumbling block appears when dealing with recipe labels as the labelRef
poses challenges unknown to the domain model.
Hence, the primary query remains how to construct a solid labelRef
structure in the Angular TypeScript setup so that the Book
can accurately de-marshal the label
links embedded within each recipe
object.