如何设置 Spring 模型以支持复杂对象的角度编辑组件?



我正在用Spring和Angular编写我的第一个Web应用程序。我使用PHP大约10年,然后在过去的10年里发现并使用了Rails,并且已经做了几个ASP项目来启动,所以我对Web开发并不陌生。我正在尝试创建我的第一组完整的 CRUD 操作,但我找不到有关如何为"复杂"对象(与父母和/或子对象(创建编辑表单的文档。官方的 Angular 指南就停在了这个位置,我在互联网上的任何地方都找不到一个教程来全面涵盖这一点。在我的示例中,我想要一个"产品"的编辑组件,我可以在其中更改当前选择的"引擎"。

在 Spring 中,我为我的两个初始模型配置了类、存储库和控制器,如下所示:

发动机(父级(->产品(子级(

Product.java

package com.xyz.cddm_ng;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import javax.persistence.*;
import java.time.LocalDateTime;
import static javax.persistence.GenerationType.IDENTITY;
@Entity
@Getter @Setter
@NoArgsConstructor
@ToString @EqualsAndHashCode
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
public class Product {
@Id
@GeneratedValue(strategy=IDENTITY)
Long id;
@NonNull String title;
String note;
@CreationTimestamp
LocalDateTime createDateTime;
@UpdateTimestamp
LocalDateTime updateDateTime;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "engine_id")
@JsonManagedReference
Engine engine;
}

Engine.java(片段(:

@OneToMany(fetch = FetchType.LAZY, mappedBy = "engine")
@JsonBackReference
Collection<Product> products;

控制器和存储库只是存根。

ProductController.java

package com.xyz.cddm_ng;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("products")
@CrossOrigin(origins = "http://localhost:4200")
class ProductController { }

ProductRepository.java

package com.xyz.cddm_ng;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;
import org.springframework.web.bind.annotation.CrossOrigin;
@RepositoryRestResource
@CrossOrigin(origins = "http://localhost:4200")
interface ProductRepository extends JpaRepository<Product, Long> { }

在角度方面,我有product.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class ProductService {
public API = '//localhost:8080';
public PRODUCT_API = this.API + '/products';
constructor(private http: HttpClient) { }
getAll(): Observable<any> {
return this.http.get(this.PRODUCT_API);
}
get(id: string) {
return this.http.get(this.PRODUCT_API + '/' + id);
}
save(product: any): Observable<any> {
let result: Observable<Object>;
if (product['href']) {
result = this.http.put(product.href, product);
} else {
result = this.http.post(this.PRODUCT_API, product);
}
return result;
}
remove(href: string) {
return this.http.delete(href);
}
}

product-edit.component.ts

import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subscription } from 'rxjs';
import { ActivatedRoute, Router } from '@angular/router';
import { ProductService } from '../shared/product/product.service';
import { EngineService } from '../shared/engine/engine.service';
import { NgForm } from '@angular/forms';
@Component({
selector: 'app-product-edit',
templateUrl: './product-edit.component.html',
styleUrls: ['./product-edit.component.css']
})
export class ProductEditComponent implements OnInit, OnDestroy {
prod: any = { };
sub: Subscription;
engines: any[] = [ ];
constructor(private route: ActivatedRoute,
private router: Router,
private productService: ProductService,
private engineService: EngineService) { }
ngOnInit() {
this.sub = this.route.params.subscribe( params => {
const id = params['id'];
if (id) {
this.productService.get(id)
.subscribe(
data => {
this.prod = data;
},
error => {
console.log(`Product with '${id}' not found, returning to list. Error was '${error}'.`);
this.gotoList();
});
this.engineService.getAll().subscribe(
data => {
this.engines = data;
});
}
});
}
ngOnDestroy() {
this.sub.unsubscribe();
}
gotoList() {
this.router.navigate(['/product-list']);
}
save(form: NgForm) {
this.productService.save(form).subscribe(result => {
this.gotoList();
}, error => console.error(error));
}
remove(href) {
this.productService.remove(href).subscribe(result => {
this.gotoList();
}, error => console.error(error));
}
}

product-edit.component.html

<form #f="ngForm" (ngSubmit)="save(f.value)">
<div class="form-group">
<label class="form-control-label" for="title">Title:</label>
<input type="text" class="form-control" id="title" required [(ngModel)]="prod.title" name="title">
</div>
<div class="form-group">
<label class="form-control-label" for="note">Note:</label>
<input type="text" class="form_control" id="note" required [(ngModel)]="prod.note" name="note">
</div>
<div class="form-group">
<label class="form-control-label" for="engine" #selectedValue>Engine</label>
<select class="form-control" id="engine" name="engine" [(ngModel)]="prod.engine">
<option [value]="null"></option>
<option *ngFor="let engine of engines" [ngValue]="engine">{{engine.name}}</option>
</select>
</div>
<button type="submit" class="btn btn-success">Submit</button>
</form>

我目前的问题是从 Spring 端生成正确的 JSON 以便在 Angular 端使用。我相信我需要在产品中包含嵌套引擎,以便可以在下拉列表中显示当前引擎。(我的理解是,[(ngModel)]="prod.engine"部分应该解决这个问题。

在尝试在产品和引擎之间创建双向关联时,我创建了一个递归循环。如果我点击http://localhost:8080/products/1,产品加载引擎,加载产品,哪个...等着吧...溢出堆栈,并导致浏览器出错并显示 500。试图解决这个问题让我想到这个问题:杰克逊双向关系(一对多(不起作用,这完全是关于@JsonIdentityInfo及其相关注释的。

所以我把这些注释放在我的模型上,但后来我收到关于"找不到名称为'id'的属性"的错误。尝试解决此问题会导致以下常见问题:Spring boot @ResponseBody 不会序列化实体 ID。那里有关于在 REST 后端公开 ID 是"错误"事情的讨论,但用@JsonIdentityInfo修复递归急加载问题似乎需要 ID。所以现在我已经解决了这个问题,并且我在 JSON 中获得了 ID,并且没有递归循环,但我仍然没有在产品的 JSON 中获取关联的引擎数据:

{
"id" : 1,
"title" : "eowir",
"note" : "noerw",
"createDateTime" : "2018-08-22T16:10:07.349752",
"updateDateTime" : "2018-08-22T16:10:07.349752",
"_links" : {
"self" : {
"href" : "http://localhost:8080/products/1"
},
"product" : {
"href" : "http://localhost:8080/products/1"
},
"engine" : {
"href" : "http://localhost:8080/products/1/engine"
}
}
}

http://localhost:8080/products/1/engine有效,http://localhost:8080/engines/1/products也是如此.因此,模型工作正常,关联正确,路由按预期工作。是什么阻止我将引擎作为产品 JSON 的一部分倾倒?

另外,如果我解决了这个问题,并将引擎节与产品 JSON 一起获取,这会使编辑表单起作用吗?这会自动使用当前关联的引擎填充引擎下拉列表吗?

所以我在Reddit上对此进行了讨论,人们引导我远离使用Spring REST的想法。我被指向 https://github.com/spring-projects/spring-petclinic,根据其自述文件,这是Spring中各种项目的长期例子。在我迄今为止的所有阅读中,我从未偶然发现这一点。

从该项目的pom.xml,我看到Spring Data JPA和Spring Web的组合提供了REST服务——完全是它本身——这就是我认为我需要Spring REST的原因。(我似乎仍然需要 Jackson@JsonIdentityInfo注释来防止我的双向一对多关系上的堆栈溢出递归循环,但我似乎不需要反向或托管引用注释。

从弹簧-宠物诊所-角度前端子项目中的组件和表单中,我还可以最终看到,我必须在下拉选择(q.v.,https://github.com/spring-petclinic/spring-petclinic-angular/blob/master/src/app/pets/pet-edit/pet-edit.component.html(中手动设置一个定义的关联对象实例,然后在将其发送到后端的更新功能之前,将该选择与服务中的父对象"重新组合"。这是我习惯于Rails为我处理的事情,我还没有看到一个例子来说明Angular是否会以某种方式为我做到这一点。答案似乎是,"不"。

最新更新