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

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.

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:
Controller/Resource/Client
Service/BusinessLogic
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:
RecipeDTO
Recipe
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

Related

Spring does not persist bi-directional OneToOne relation on owning side

In my spring boot project I have a Document class that has a bi-directional OneToOne relationship to an Invoice class, which share the same ID.
Document
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne(cascade = CascadeType.ALL)
#PrimaryKeyJoinColumn
private Invoice invoice;
Invoice
public class Invoice {
#Id
#Column(name = "document_id")
private Long documentId;
#OneToOne(mappedBy = "invoice")
#MapsId
#JoinColumn(name = "document_id")
private Document document;
The document entity is created prior to the invoice entity. Later on I create an invoice entity via a MapStruct DTO-Mapping. I then save the entity to "generate" the document_id value.
After saving the invoice entity, I assign the invoice entity to the document entity and save the document entity via the repository. However, the relation to the invoice entity is not persisted in the database.
The invoice entity persists as should be with the corresponding document_id as primary key.
Service code
Invoice newInvoice = invoiceMapper.fromDto(dto);
newInvoice = invoiceRepository.save(newInvoice);
document.setInvoice(newInvoice);
documentRepository.save(document);
InvoiceMapper
#Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE, uses = {DocumentService.class})
public interface InvoiceMapper {
#BeanMapping(ignoreByDefault=true)
#Mapping(source = "document", target = "document")
Invoice fromDto(Dto dto);
Previously, I tried mapping the document_id in the MapStruct mapper aswell, but then I received an "attempted to assign id from null one-to-one property" exception on save (even though document and document_id were correctly defined).
When debuggin the code, it correctly shows that the invoice entity was set on the document entity, but unfortunately it is not persisted in the database.
Curiously, I am almost certain that at some point in the coding process it did work as intended. But I can not figure out where the issue is. Help would be much appreciated.
This is not setup correctly as you have not specified anything to set the Invoice's document_id column - you'd have to set this yourself from the documentId long. You must pick one side to have a foreign key to the other - presumably the Invoice has the foreign key to Document and is going to use that as its primary key as well. If that is the case, this needs to be:
public class Invoice {
#Id
#Column(name = "document_id")
private Long documentId;
#OneToOne //this is the owning side of the relationship!
#MapsId
#JoinColumn(name = "document_id")
private Document document;
..
}
public class Document {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne(mappedBy = "document")
private Invoice invoice;
..
}
MappedBy indicates that the other side controls setting the foreign key values. Note though that your Invoice will not have a documentId value set, and that you do not need to manually set it. The MapsId annotation tells JPA to pull the value from the Document ID when it is generated and to use that for the document_id column, and will set the documentId Long at the same time.
This then will allow you to create a new Document and Invoice and just call save on the document - once. It isn't enough to just add an invoice to the document - the ID within Invoice is entirely controlled by the Invoice.document reference, so if it isn't set, it will be left null. You must maintain both sides of bidirectional relationships yourself to have the model in synch with what you want in the database. Or at least set the owning side of any bidirectional relationship.

Race Condition in Postgres SQL using Spring data JpaRepository

I am facing a wierd issue in my implementation where I am persisitng data to a PostgresSQL DB using Spring data JpaRepository
In my Entity class I have the below columns:
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", unique = true, nullable = false)
private int id;
#Column(name = "field1", nullable = false, length = 16)
private String field1;
#Column(name = "field2", nullable = false, length = 16)
private String field2;
#Column(name = "field3", nullable = false, length = 16)
private String field3;
I initially avoided declaring the fields above as composite since there were many fields to be dealt with as composite keys. I thought the java code check would do the trick in all scenarios
So basically, I have to maintain the uniqueness of each row based on field1,field2 and field3. That was the basic requirement for which I had checks in my java code that if any entry exists in the DB for the combination of field1,field2 and field3 then I used to throw java exceptions
No two rows can have these values repeating. All was good until the application was tested under some errorneous business scenarios which would never happen in production but got run by mistake
Whats happening now is that if 2 requests are triggered at the exact same instance with the exact same 3 fields above (through a script) then they both enter into the Database since both get the entry check as false
Would declaring all of them as one composite key resolve the situation?
You should define the unique constraint in your database in addition of JPA constraint.
#Entity
#Table(uniqueConstraints={
#UniqueConstraint(columnNames = {"field1", "field2", "field3"})
})
public class MyEntity {
...
}

Spring REST api - foreign key ID instead of entire object

I have a problem with Json returned by my REST api GET method.
That's how my entities looks like:
#Entity
public class Employee {
...
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "department_id", nullable = true)
private Department department;
}
#Entity
public class Department {
...
#OneToMany(mappedBy = "department", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JsonBackReference
private Set<Employee> employees;
}
And this is the answer I get, while I'm trying to GET Employee by its id:
{
"id": 1,
"surname": "smith",
"department": {
"id": 1,
"name": "HR",
"room": "13"
}
}
Now, Instead entire Department object, I would like to get just simple id: "department_id": 1, and I don't know how to do that.
Second question: what's the good practise in this situation in REST api? Should I leave it like it is; expose only id (what I'm asking you how to do); or use DTO and not showing it at all?
Moreover, anyway I'm going to add _links to this user's department, and in this case i thought that leaving only id should be ok (tell me if I'm wrong).
Looking forward for your answers!
A good practice is to define a DTO that represents the data that is exposed by your API.
This should be decoupled from your domain (Employee) as it will offer you more flexibility, just like what you want to achieve.
class EmployeeDTO extends RepresentationModel {
private long id;
private String surname;
private long departmentId;
// getters and setters
}
This should work. Of course you need to map your Employee entity to the EmployeeDTO. RepresentationModel contains the _links property that you want for the HATEOAS (for example, have a look at
https://www.baeldung.com/spring-hateoas-tutorial )
About exposing the id from your database, I think that a good reason for not doing it is that you are giving information about your database size for free and it's something that you might not want to. More information could even be derived from that.
Here you can find a good discussion on the topic:
Exposing database IDs - security risk?
I would suggest to have a look at UUID which is a universally unique alphanumeric identifier that doesn't expose this information about your data.
More about UUID: https://www.baeldung.com/java-uuid
#JsonIgnoreProperties
To just get department id without changing any implementation you may use #JsonIgnoreProperties({"name", "room"}) as following
#ManyToOne(fetch = FetchType.EAGER, optional = false)
#JoinColumn(name = "department_id", nullable = true)
#JsonIgnoreProperties({"name", "room"})
private Department department;
which will respond with the following
[
{
"id": 1,
"surname": "smith",
"department": {
"id": 1
}
}
]
You may also like to explore other ways to achieve the same here
Best Practices
We should never expose and return our modal and entities as a response to APIs. We may create the DTOs/DAOs to receive and transfer the objects and data. You may also convert the entity to DTO and DTO to entity using mappers.
In the case of DTO, you may just include the department id and may fetch the object if required using the repository.

How to link an entity from one table into another?

How can I link an entity that already exists in another table into my important_table? I could insert the ID but then that would require a query. What I want is that the system automatically maps the element in the people_table to the important_table.
#Entity(name = "important_table")
data class ImportantEntity(
#Id #GeneratedValue #Column(name = "id")
val id: Short = 0,
#Embedded
val person: Person
)
Person Entity
#Entity(name = "person_table")
data class PersonEntity(
#Id #GeneratedValue #Column(name = "id")
val id: Long = 0,
...
)
I tried embedded but that creates a duplicate Person in the db. I want the link so that I can find "important" people easy and still get the same data.
Use a #OneToOne mapping to let Hibernate (or any ORM) knows you are linking the tables together.
#Entity(name = "important_table")
data class ImportantEntity(
#Id #GeneratedValue #Column(name = "id")
val id: Short = 0,
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
val person: Person
)
When you say: I could insert the ID but then that would require a query, indeed, it does... But with the FetchType.EAGER, it's your ORM that will execute the extra query for you... It is transparent.
Nonetheless, be careful with that, if you don't pay attention to your relationships, you can end up loading the entire db in memory...
As explained in this site, and this tuto, #Embedded is used to help having a nice and clean object definition, while storing the data into one table instead of 2... For instance, in your case, you would be storing the data from the class Person into the table ImportantTable.
Hope it helps !

one-way one-to-many throws Hibernate Cannot add or update a child row: a foreign key constraint fails

I have an application that teaches the user how to play various card games. The data model that gets persisted consists of a TrainingSession with a uni-directional one-to-many relationship with the Hands.
[EDIT] To clarify, a Hand has no existence outside the context of a TrainingSession (i.e they are created/destroyed when the TrainingSession is). Following the principals of Data Driven Design, the TrainingSession is treated as an aggregate root and therefore a single spring-data CrudRepository is used (i.e., no repository is created for Hand)
When I try to save a TrainingSession using a CrudRepository, I get: com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Cannot add or update a child row: a foreign key constraint fails (blackjack.hand, CONSTRAINT FKrpuxac6b80xc7rc98vt1euc3n FOREIGN KEY (id) REFERENCES training_session (tsid))
My problem is the 'save(trainingSession)' operation via the CrudRepository instance. What I don't understand is why the error message states that FOREIGN KEY (id) REFERENCES training_session (tsid)). That seems to be the cause of the problem but I cant figure out why this is the case or how to fix it. The relationship is uni-directional and nothing in the Hand class refers to the TrainingSession.
The code, minus all the getters and setters, is:
#Entity
public class TrainingSession {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer tsid;
private String strategy;
#OneToMany(cascade=CascadeType.ALL)
#JoinColumn(name="id")
private List<Hand> hands;
private int userId;
protected TrainingSession() {
}
public TrainingSession(int userId, Strategy strategy, List<Hand> hands) {
this.strategy = strategy.getClass().getSimpleName();
this.hands = hands;
this.userId = userId;
}
while Hand is
#Entity // This tells Hibernate to make a table out of this class
public class Hand {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
private int p1;
private String p1s;
private int p2;
private String p2s;
private int d1;
private String d1s;
private int trials;
private int score;
public Hand() {
}
You need to save your TrainingSession and Hand objects first before saving the adding the hand objects to TrainingSession.
TrainingSession ts1 = new TrainingSession();
trainingSessionManager.save(ts1);
Hand hand1 = new Hand();
handManager.save(hand1);
Hand hand2 = new Hand();
handManager.save(hand2);
ts1.gethands().add(hand1);
ts1.gethands().add(hand2)
trainingSessionManager.save(ts1);
If you check your database you will find 3 tables TrainingSession, Hand and TrainingSession_Hand, The TrainingSession_Hand table references to both TrainingSession and Hand both. Therefore you need to save TrainingSession and hand before saving the relationship.
Found the problem. I was assuming that when spring-data set up the DB tables, it was able to figure out and set up the uni-directional 1-to-many relationship. Apparently that isn't the case. When I configure the relationship as bi-directional everything seems to work.
To fix things I:
removed from TrainingSession the #joincolumn annotation for hands
in Hands I added a TrainingSession field with a #ManyToOne annotation:
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "tsid", nullable = false)
#OnDelete(action = OnDeleteAction.CASCADE)
private TrainingSession tsession;
I also added in the Hand class the getter/setter for tsession
I can now do a save of the entire aggregate construct using only a TrainingSessionRepository.

Resources