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

Time:02-01

I'm struggling to find what feels like a good design for a Spring Boot CRUD REST API app that involves several OneToMany relationships w/ join tables. For example, consider this DB structure in MySQL which allows one "Recipe" to be associated with several "Recipe Categories":

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
);

On the Java side, I'm representing the structures as JPA entities:

@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 ...
}

This works OK, but my hang-up is that to persist/save a new Recipe via REST JSON API, the caller needs to know about the join table recipe_category_associations. For example a PUT request w/ this payload could add a new Recipe to the DB associating it with the "category foo" recipe category:

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

Using this in the controller:

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

To me, the inclusion of "recipeCategoryAssocations" key in the JSON payload feels weird. From the client POV, it doesn't really need to know the mechanism creating this association is a join table. Really, it just wants to set a list of recipe category ids like:

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

Any tips how best to accomplish this in nice way? It'd be nice if I can keep the REST implementation super clean (e.g., like I have now with one recipeRepository.save(recipe); call). Thanks in advance.

CodePudding user response:

When writing software we expect requirement to change. Therefore we want to make sure our code will be flexible and easy to evolve.

Coupling our server response with our DB structure makes our code very rigid. If a client needs a new field or if we want to arrange the DB schema differently everything will change.

There are several different approaches to designing your software correctly. A common approach is called "Clean Architecture" and is outlined in a book by this title by the great Uncle Bob. The Book itself outlines the approach in high level but there are many example projects online to see what it means in action.

For example this article by my favourite Java blog: [baeldung.com/spring-boot-clean-architecture][1]

If you are looking for something simpler, you can follow the ["3-Tier Architecture"][2] (not really an architecture in my mind). Separate your code in to 3 layer:

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

Each layer will use a different data object. the business logic layer will have the object in it's purest form without constraints regarding who will want to read it and where it is stored and will be mapped/converted to the objects in the other layers as needed.

So in your case you might have 3 (or more) different objects:

  1. RecipeDTO
  2. Recipe
  3. model.Recipe (and model.RecipeCategoryAssociation etc.)

Make sure that the Business level object only have fields that makes sense from a business logic. The code in each layer will use the objects that are relevant to that layer. When a rest controller class for example calls the business logic server it will need to convert the DTO object to the Business level object for example. Very important to maintain this separation between layers

  •  Tags:  
  • Related