Mapping Bidirectional Object Associations using MapStruct
Photo by Florida-Guidebook.com on Unsplash

Mapping Bidirectional Object Associations using MapStruct

In object-oriented programming,?an association is a relationship between two entities that defines how two objects communicate. An association can be?one-to-one,?one-to-many,?many-to-one?or?many-to-many.

Bi-directional associations?happen when two classes define their relationships with each other symmetrically (symmetrical associations).

In this article, we will explore two mapping strategies, between domain objects and JPA Entities with their relationships, using MapStruct.

Before we start, please note that it is assumed that you already have a basic understanding of MapStruct and JPA.


Association Relationship

Let’s look at the following Entity-Relationship diagram:

No alt text provided for this image
ERD for Orders

In this example, we can see the domain model for orders placed in a restaurant.?

Three tables contain the data about an order:

  • A parent table with the order information (ORDER_DETAIL).
  • Two child tables, one containing consumer information (ORDER_CONSUMER) and one containing ordered items (ORDER_ITEM).

Parent-Child Associations

Order (the parent) has a one-to-one relationship with Consumer (meaning an order is associated with a single consumer) and a one-to-many relationship with OrderItem (meaning each order can have multiple items).

@Entity
@Table(name = "order_detail")
@Access(AccessType.FIELD)
public class Order {

    @Id
    @GeneratedValue
    @Column(name = "order_id", columnDefinition = "BINARY(16)")
    private UUID id;

    @OneToOne(mappedBy = "order", cascade = CascadeType.ALL, 
            fetch = FetchType.LAZY, orphanRemoval = true)
    private ConsumerJpaEntity consumer;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, 
            fetch = FetchType.LAZY, orphanRemoval = true)
    private Set<OrderItemJpaEntity> items;

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    private OrderState state;

    @Column(columnDefinition = "text")
    private String notes;

    private Instant createdAt;

    private Instant updatedAt;

    // Getters and Setters removed for simplicity
}        

Here, we model the association for the consumer field with the @OneToOne annotation and for items with @OneToMany.

Both associations have the property fetch set to FetchType.LAZY, loading data only when necessary.

The associations also have the cascade property set to CascadeType.ALL, allowing operations to be performed on the parent and replicated in its associated child objects.

Child-Parent Associations

As each order is associated with a single consumer, Consumer and Order have a one-to-one relationship.

@Entity
@Table(name = "order_consumer")
@Access(AccessType.FIELD)
public class Consumer {

    @Id
    @GeneratedValue
    @Column(name = "consumer_id", columnDefinition = "BINARY(16)")
    private UUID id;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private OrderJpaEntity order;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private String address;

    @Column(nullable = false)
    private String phone;

    // Getters and Setters removed for simplicity
}        

This association is modeled with the annotations @OneToOne and @JoinColumn(name="order_id").


In the following code example, each OrderItem is associated with a specific Order, in a many-to-one relationship.

@Entity
@Table(name = "order_item")
@Access(AccessType.FIELD)
public class OrderItem {

    @Id
    @GeneratedValue
    @Column(name = "item_id", columnDefinition = "BINARY(16)")
    private UUID id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private OrderJpaEntity order;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false)
    private Integer quantity;

    // Getters and Setters removed for simplicity
}        

Here, we model the association using @ManyToOne and @JoinColumn(name="order_id").


Mapping Strategies

First Strategy: Using @AfterMapping

The basic idea of this strategy is to map the attributes of a domain object or a DTO to the JPA Entity, and after that, to establish a bi-directional — symmetrical — relationship between the child entities and their parent.

@Mapper
public interface OrderServiceMapper {
    
    @Mapping(target = "items", qualifiedByName = "orderItemDtoSetToOrderItemSet")
    Order orderDtoToOrder(OrderDto order);

    @IterableMapping(qualifiedByName = "orderItemDtoToOrderItem")
    @Named("orderItemDtoSetToOrderItemSet")
    Set<OrderItem> orderItemDtoSetToOrderItemSet(Set<OrderItemDto> list);
    
    @Named("orderItemDtoToOrderItem")
    OrderItem orderItemDtoToOrderItem(OrderItemDto item);

    @AfterMapping
    default void setOrder(@MappingTarget Order order) {

        Optional.ofNullable(order.getConsumer())
                .ifPresent(it -> it.setOrder(order));

        Optional.ofNullable(order.getItems())
                .ifPresent(it -> it.forEach(item -> item.setOrder(order)));
    }
}        

In the OrderServiceMapper interface, setOrder() is annotated with @AfterMapping. This method will be invoked at the end of the mapping method orderDtoToOrder() (right before the last return statement).

The setOrder() method has a parameter annotated with @MappingTarget that holds the target of the mapping method, in this case, an instance of Order. In this step, we establish the symmetrical relationship between Order and its child objects: Consumer and OrderItem.

Second Strategy: Using Adders and?Setters

In the second strategy, we map the attributes of a domain object or a DTO to the JPA Entity, similar to what was presented in the first strategy. The main difference is that we modify the parent entity to define a bi-directional — symmetrical — relationship with its child entities.

@Entity
@Table(name = "order_detail")
@Access(AccessType.FIELD)
public class Order {

    // Fields removed for simplicity

    public void setConsumer(Consumer consumer) {
        this.consumer = consumer;
        consumer.setOrder(this);
    }

    public void addItem(OrderItem item) {
        if (this.items == null) {
            this.items = new HashSet<>();
        }
        items.add(item);
        item.setOrder(this);
    }

}        

Here, the setConsumer() method sets the Consumer in a one-to-one symmetrical relationship.

To add entities with one-to-many associations, we have created an adder. The addItem() method initializes the collection if it’s null, adds items to the collection and sets the symmetrical relationship.

@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface OrderServiceMapper {

    @Mapping(target = "items", qualifiedByName = "orderItemDtoToOrderItem")
    Order orderDtoToOrder(OrderDto order);
    
    @Named("orderItemDtoToOrderItem")
    OrderItem orderItemDtoToOrderItem(OrderItemDto item);
}        

In the OrderServiceMapper interface, we have to tell MapStruct to map collections using adders. In order to do this, the @Mapper annotation has the collectionMappingStrategy property set with the value CollectionMappingStrategy.ADDER_PREFERED.


Summary

In this article, we explored how to map bi-directional object associations using MapStruct.

We looked at a strategy that uses the @AfterMapping annotation. This is a good fit if you don’t want to add any logic in your target class. The complete code of the first strategy is available on GitHub.

If you already have logic in your target class or even if you don’t want to add MapStruct to your project, the second strategy might be best for you. The complete code of this strategy is available on GitHub.

Thanks for reading. I hope this was helpful!

要查看或添加评论,请登录

Jonathan Manera的更多文章

  • Service Discovery Patterns with Netflix Eureka

    Service Discovery Patterns with Netflix Eureka

    Modern distributed systems assign network locations dynamically, so you don’t know what IP address or port each service…

  • Spring Cloud Circuit Breaker Resilience4j

    Spring Cloud Circuit Breaker Resilience4j

    A distributed system, which comprises many services interacting to achieve business goals, is prone to failures in the…

  • Building Microservices with Spring Boot and?gRPC

    Building Microservices with Spring Boot and?gRPC

    gRPC Remote Procedure Call is a binary message based protocol for writing cross-language applications. As part of this…

  • Aspect Oriented Programming for?Babies

    Aspect Oriented Programming for?Babies

    Aspect Oriented Programming is a powerful tool that enables a clean modularization of “crosscutting” concerns of…

  • 7 Best Practices for Java API Documentation

    7 Best Practices for Java API Documentation

    The Javadoc Tool simplifies the documentation process in Java, allowing developers to document their code seamlessly as…

  • The Holy Trinity: JUnit5, Mockito, and Instancio

    The Holy Trinity: JUnit5, Mockito, and Instancio

    Unit testing is a software testing technique where individual components of an application (units) are tested in…

  • Code Quality Analysis with Sonar

    Code Quality Analysis with Sonar

    SonarQube is an open-source platform for continuous code quality inspection. This tool provides a detailed analysis of…

  • API-First Design with OpenAPI Generator

    API-First Design with OpenAPI Generator

    APIs are contracts between services and their clients. These contracts are usually documented using an interface…

    1 条评论
  • Testing Microservices with Testcontainers

    Testing Microservices with Testcontainers

    When writing integration tests for Spring Boot applications, we usually need to access external resources such as…

  • 10 Meditations for Creative Professionals

    10 Meditations for Creative Professionals

    Sometimes, in our day-to-day life, we feel that something is not quite right. We find it hard to be as productive as we…

社区洞察

其他会员也浏览了