Dec 23, 2022

Use jpa relations mindfully

JPA is a great tool to map your entities to tables but its modeling paradigm through annotations leads to a complex spaghetti-mess. A simpler and way better modeling approach for relations is to just use plain ids instead of referencing another entity.

@Entity(name = "user")public class User {    @Id    private UUID id;    // Use this    private UUID organizationId;    // And NOT this    @ManyToOne    @JoinColumn(name="organization_id")    private Organization organization;}

One of the most important benefits of this simplified modeling approach is that if you think of your entity and all its fields as its state then it is now easy to realize that a state change of the organization does not mean a state change of the user. This might sound obvious but if you use jpa relations all the time you have to know which referenced objects are part of the user entity and which are not. We are reducing the cognitive load to reason about the user entity which in turn reduces the overall complexity of your code base.

The user entity can now be easily tested without creating every referenced entity. We minimize the risk of spaghetti code that manipulates the organization within the user. We further improve the loading performance of users as these kinds of many-to-one relations are usually fetched eagerly.

Using plain ids should be your default. Strongly referencing other entities should be the exception.

A case for strong references

If the relation is exclusive to one entity you might use strong references.

public class Invoice {    @Id    private UUID id;    @OneToMany(cascade=ALL, orphanRemoval=true, fetch=EAGERLY)    @JoinColumn(name="invoice_id")    private List<InvoiceItem> items;    private MonetaryAmount sum;}

This way of modeling gives clear responsibility over the life-cycle of invoice items to the invoice. Invoice items have to be created, deleted or updated through the invoice entity which then updates its sum accordingly. This also implies that you should not provide a repository for invoice items. You should query the items by loading the invoice.

The main difference is here that these items are only strongly referenced by the invoice and this reference is exclusive to the invoice. Changing the state of an item changes the state of the invoice.

Be clear about the ownership an entity has over another entity. Use weak references by id as a default.

A quick tip for the view layer

Some people might suggest that you should create relations with strong references if the view requires it. This argument forces view concerns into your modeling approach which quickly leads to the N+1 query problem. It also weakens your service api by hiding client requirements through traversing strong references to load data.

// Do not model your relations only to implement these view requirementspublic class UserController {    public List<UserDto> listUsers() {        return userService.list().stream().map(user -> UserDto(  ,            OrganizationDto(      ,                  )        )).toList();    }}

The better approach is the following:

public class UserController {    public List<UserDto> listUsers() {        return userService.list().stream().map(user -> UserDto(  ,            user.organizationId        )).toList();    }}public class OrganizationController {    public List<OrganizationDto> listOrganizations(        final Set<UUID> ids    ) {        return organizationService                    .listByIds(ids)                    .stream()                    .map(organization ->                        OrganizationDto(                  ,                                          )                    ).toList();    }}

Now we model query requirements explicitly in the service layer and keep our coupling in the domain model to the minimum. We also provide a better generic api for our clients. With changing requirements in the frontend we can freely compose all the data we need without being scared of slow performance through over-fetching.

In this example the requirement to show the organizations name in the frontend on the user list view might just be removed. If these kinds of changing frontend requirements happen we do not have to change our backend api. We reduce coupling between frontend and backend requirements.

My advice going on

Reference other entities by id to decouple your code base. Only use jpa relations if there is a business rule supporting strong coupling. An indicator for this is if cascading deletes should happen or updates in the entity should immediately update state in the referenced object. When using jpa relations make sure that only a single entity owns the relation and no other entity is using the same entity reference with strong coupling.

I'd love to hear your opinions on this. Contact me per mail or on Twitter. Keep on creating awesome stuff.