具有 OneToMany 关系的 Spring Boot CRUD REST API 的最佳设计模式

Best design pattern for Spring Boot CRUD REST API with OneToMany relationships

我正在努力为 Spring Boot CRUD REST API 应用程序寻找一个好的设计,该应用程序涉及多个 OneToMany 关系 w/join tables。例如,考虑 MySQL 中的这个 DB 结构,它允许一个“食谱”与多个“食谱类别”相关联:

create table recipes
(
    id int auto_increment primary key,
    name varchar(255)
);

create table recipe_categories
(
    id int auto_increment primary key,
    name varchar(64) not null
);

create table recipe_category_associations
(
    id int auto_increment primary key,
    recipe_category_id int not null,
    recipe_id int not null,
    constraint recipe_category_associations_recipe_categories_id_fk
        foreign key (recipe_category_id) references recipe_categories (id)
            on update cascade on delete cascade,
    constraint recipe_category_associations_recipes_id_fk
        foreign key (recipe_id) references recipes (id)
            on update cascade on delete cascade
);

在 Java 方面,我将结构表示为 JPA 实体:

@Entity
@Table(name = "recipes")
public class Recipe {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id", nullable = false)
  private Integer id;


  @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL)
  @JsonManagedReference
  private Set<RecipeCategoryAssociation> recipeCategoryAssociations;

  // ... setter/getters ...
}
@Entity
@Table(name = "recipe_categories")
public class RecipeCategory {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id", nullable = false)
  private Integer id;

  @Column(name = "name", nullable = false)
  private String name;

  // ... setter/getters ...
}
@Entity
@Table(name = "recipe_category_associations")
public class RecipeCategoryAssociation {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  @Column(name = "id", nullable = false)
  private Integer id;

  @ManyToOne(optional = false, fetch = FetchType.LAZY)
  @JoinColumn(name = "recipe_category_id", nullable = false)
  private RecipeCategory recipeCategory;

  @ManyToOne(optional = false, fetch = FetchType.LAZY)
  @JoinColumn(name = "recipe_id", nullable = false)
  @JsonBackReference
  private Recipe recipe;

  // ... setter/getters ...
}

这工作正常,但我的挂断是通过 REST JSON API persist/save 一个新的 Recipe,调用者需要知道加入table recipe_category_associations。例如,带有此有效负载的 PUT 请求可以将新配方添加到数据库,并将其与“类别 foo”配方类别相关联:

{
  "name": "Chicken soup",
  "recipeCategoryAssociations": [{
    "recipeCategory": {
       "id": 123,
       "name": "category foo"
    }
  }] 
}

在控制器中使用这个:

  @PutMapping(path = PATH, produces = "application/json")
  @Transactional
  public @ResponseBody Recipe addNewRecipe(@RequestBody Recipe recipe) {
    return recipeRepository.save(recipe);
  }

对我来说,JSON 有效负载中包含“recipeCategoryAssocations”键感觉很奇怪。从客户端 POV 来看,它实际上不需要知道创建此关联的机制是一个连接 table。实际上,它只是想设置一个食谱类别 ID 列表,例如:

{
  "name": "Chicken soup",
  "recipeCategories": [123, 456, ...] 
}

有什么技巧可以很好地完成此任务吗?如果我可以保持 REST 实现超级干净(例如,就像我现在有一个 recipeRepository.save(recipe); 调用),那就太好了。提前致谢。

在编写软件时,我们期望需求会发生变化。因此,我们希望确保我们的代码灵活且易于发展。

将我们的服务器响应与我们的数据库结构相结合使我们的代码非常严格。如果客户需要一个新字段,或者如果我们想以不同的方式安排数据库模式,一切都会改变。

有几种不同的方法可以正确设计您的软件。一种常见的方法称为“清洁架构”,伟大的鲍勃叔叔在同名的书中对此进行了概述。这本书本身概述了高层次的方法,但网上有许多示例项目可以了解它的实际意义。

例如我最喜欢的 Java 博客的这篇文章: [baeldung.com/spring-boot-clean-architecture][1]

如果您正在寻找更简单的东西,您可以遵循 ["3 层架构"][2](在我看来这不是一个真正的架构)。将您的代码分成 3 层:

  1. Controller/Resource/Client
  2. Service/BusinessLogic
  3. Repository/DataAccess

每一层都会使用不同的数据对象。业务逻辑层将拥有最纯粹形式的对象,不受关于谁想要读取它以及它存储在哪里的限制,并且将 mapped/converted 根据需要提供给其他层中的对象。

因此在您的情况下,您可能有 3 个(或更多)不同的对象:

  1. 食谱DTO
  2. 食谱
  3. model.Recipe(以及 model.RecipeCategoryAssociation 等)

确保业务级别对象仅包含对业务逻辑有意义的字段。每层中的代码将使用与该层相关的对象。例如,当休息控制器 class 调用业务逻辑服务器时,它需要将 DTO 对象转换为业务级别对象。保持层与层之间的这种分离非常重要