Week 10 of coding bootcamp was marked by lots of junk food, a sleepless Sunday night from drinking my coffee too late, and oh yeah learning about Spring JPA entities. Learning them felt like mental gymnastics for me. Now that my brain has shakily landed from its endless flips and rumination, I wanted to share my basic understanding of the how behind each. đ
Table of Contents
đ¸ One to Many
đˇ One to One
đź Many to Many
One to Many
Below we have two entities from the basic backend framework of an ecommerceâs appâs backend. A customer has a one to many relationship with orders. In other words, a customer can buy multiple orders.
Order | Customer |
---|---|
@ManyToOne annotation above private Customer customer indicates many Order instances can belong to one Customer instance. |
@OneToMany annotation above private List<Order> orders means that one Customer instance can have multiple Order instances. |
@JoinColumn indicates the foreign key of customer_id references the one in Customer table. nullable=false means that each order must have a customer instance (when being created/updated), or in other words belong to a customer. |
mappedBy= "customer" (written above List<Order> orders ) means that the list of orders is mapped to each customer via the Customer customer field on the Orders table (aka⌠the Orders table owns the relationship). |
Hibernate then fetches the associated Customer instance based on that foreign key. This is how the order(s) are associated with the respective customer. |
Remember how in the Order table, Hibernate gets the Customer instance via the foreign key and that is how itâs associated with those orders? |
*Note: Even though we donât see a list of orders or the customer object - we can access that relationship programmatically via JPA, as discussed above.
Here are how the Order and Customer Table look respectively on Postgres.
@JsonIgnore
The @JsonIgnore
is to prevent âloopingâ (aka circular dependency) when we display the data. For example, when we made a request to get orders tied to a customer, without JSON, it would show the order, then the customer, which then shows the customer field info and then calls the order field which then calls the customer, ad infinitum.
With @JsonIgnore
- only the Order information would show when making that get
request.
When deciding where to place @JsonIgnore
- you can think of it as we donât need to show the customer when querying orders, but it would be more helpful to show the orders when querying a customer. An alternative way of approaching that thought process is: we put @JsonIgnore
over the customer
field, because a customer can exist without orders but not vice versa.
Owning Table
Orders is the owning table, which means any changes to the relationship, such as adding or removing orders for a customer, should be made through the customer field in the Order entity.) The owning side is also where the relationship is persisted in the database; that is the owning side is the one that owns the foreign key in the relationship.
We chose orders as the owning side, because of operational logic. You typically create orders and associate with customers, not the other way around.
The example code block shows how we add an order to a customer. order
represents the new order we are adding (sent via the request body in the API call @PostMapping("/customers/{id}/orders")
).
public Customer addOrderToCustomer(Integer id, Order order) throws Exception {
Customer customer = customerRepository.findById(id).orElseThrow(() -> new Exception("Customer not found"));
order.setCustomer(customer);
orderRepository.save(order);
return customer;
}
Notice how the updates in the code are done via the orderâs side. The customer that has made the order is set to the customer field in the Order entity. This Order
instance that now has the respective customer tied to it is then saved (persisted) to the Order
database.
Bidirectional
As a side note, as cascade persist is not used above, you can set the order on the customer side as well to allow for bidirectional navigation. Cascade persist means that operations on say a customer entity would persist to the associated order entities (i.e. if you deleted a customer, then the associated customer would aso delete) âŚhowever, in our case- as we are primarily accessing orders from the orders side (i.e. we typically retrieve orders and associate them with customers), then we donât need that bidirectional code added.
Note:
Keep in mind, for unidirectional one-to-many relationships, in most cases- you have to set both entities "to each other", in other words for example set a team to a player, and player to a team. On the other hand, many-to-many relationships are bidirectional and that means if you set a post to have a tag, you don't need to set the post to the tag- since JPA handles that on the backend (made possible by the associations between both entities via the join table).
One to One
Below we see how each record in our Address entity is associated with one record in the Customer entity., and vice versa. That is, in our hypothetical scenario: each customer can have one address, and one address can belong to one customer.
All the annotations are handled on the Customer entity side:
-
@OneToOne
annotation in our Customer entity aboveprivate Address address
means that one customer instance has one address -
CascadeType.ALL
in our Customer entity means that for any actions taken on aCustomer
instance, should also be applied to theAddress
. For example, if a customer is deleted, then its respective address will also be deleted. Likewise, if a customer is created, then it will be expected to have an address added. -
@JoinColumn
annotation indicates theaddress_id
is the foreign key. -
nullable=false
means theaddress_id
canât be null, or in other words each customer needs to have an address associated with it. When creating or updating a user, it must have an address. -unique=true
means that for each customer, they must have a unique address.
This screenshot below shows the Customer table in Postgres, and you can see the foreign key address_id as the first column- essentially allowing this table to âlinkâ to the Address table.
Many to Many
Below we see a relationship where one order can have multiple products (i.e. TV, sofa, etc), and one product can have many orders. In other words, orders and products have a many to many relationship.
Order | Product |
---|---|
@ManyToMany above List<Product> means one order can have multiple products. |
@ManyToMany above List<Order> means one product can have multiple orders. |
@JoinTable is on the Order side. We query most from Orders and when we query, it allows the related products to properly display. |
@JsonIgnore is placed over List<Order> to prevent that circular reference data loop. Also, we don't need a product result to show affiliated orders, whereas we want to show orders with their affiliated products. |
inverseJoinColumn means the foreign key of product_id references the one in the Product table (opposite side table). Hibernate then fetches the associated Product instance based on that foreign key. This is how the orders(s) are associated with the respective products. |
mappedBy = âproductsâ means Orders is the owning side; you create Orders and associate products to orders. The list of orders is mapped to products via the Order table (through its join table). Remember how in the Order table, Hibernate gets the Product instance via the foreign key and that is how itâs associated with those orders? |
*This below screenshot shows the Join Table that is actually created in Postgres from that @JoinTable
annotation.
Side note:
- On the Order table:
=new ArrayList<>
initializes the list of products with an empty array list, so that as products are added to an order, that array list is updated. This ensures that theproducts
field is not null even if no products are associated with that order at initialization time. - On the Product table:
=new ArrayList<>
initializes the list of orders with an empty array list, so that as new orders are made with that product, then those orders can be added in. This ensures that theorders
field is not null even if no orders are associated with that product at initialization time.