1:n relationships in Hilla with AutoGrid and AutoForm

René Wilby | Apr 19, 2024 min read

Entities in business applications

Entities play an important role in the development of business applications for me. Before I start to develop, I first think about the business entities involved and their relationships to each other. An Entity-Relationship Model is a useful tool for describing and graphically representing the relationships between entities. A very typical relationship between entities is the 1:n relationship. In an e-commerce application, for example, a customer has no, one or any number of orders and an order is usually assigned to exactly one customer. Another example would be the relationship between an invoice and the invoice items it contains: An invoice consists of one or more invoice items, and an invoice item is assigned to exactly one invoice.

A suitable technology stack

When developing business applications, it is therefore important that the frameworks used support this typical relationship as conveniently and flexibly as possible. When developing business applications with Hilla, these requirements are met very well.

Spring Boot is used in the backend. In conjunction with relational databases, you can use Spring Data JPA and benefit from many advantages, including the following:

  • The database schema can be generated automatically from the classes of the entities.
  • Communication with the database can take place via the very powerful JPA repositories.

In the frontend, the application can be developed very quickly using the ready-made UI components of Hilla. For example, a typical master-detail UI pattern can be quickly implemented using the AutoGrid and AutoForm components.

Special characteristics of the 1:n relationship

Hilla already supports the 1:1 relationships of entities in the AutoGrid component out-of-the-box (see Example). For 1:n relationships, you can easily customize both the AutoGrid and AutoForm components to support the relationship in a meaningful way.

Example

In the following example, this is shown using the entities Order and Customer. A customer has no, one or any number of orders and an order is assigned to exactly one customer. In the example, we first look at the 1:n relationship from the perspective of the order.

Backend

In the backend, the two entities Order and Customer are created first.

@Entity
@Table(name = "orders")
public class Order {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    Long id;

    @NotNull
    private String number;

    @NotNull
    private LocalDate created;

    @NotNull
    @ManyToOne(targetEntity = Customer.class, optional = false)
    private Customer customer;

    // Getter and Setter omitted
}
@Entity
@Table(name = "customers")
public class Customer {

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    Long id;

    @NotBlank
    String name;

    @NotBlank
    @Email
    String mail;

    // Getter and Setter omitted
}

Afterwards, the corresponding JPA repositories are created.

public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order>  {
    List<Order> findAllByCustomerId(Long customerId);
}
public interface CustomerRepository extends JpaRepository<Customer, Long>, JpaSpecificationExecutor<Customer> { }

Communication between the backend and frontend takes place via corresponding services for Order and Customer, which are annotated with @BrowserCallable. This is sufficient for Hilla to generate the required TypeScript classes, which can then be used in the frontend.

@BrowserCallable
@AnonymousAllowed
public class OrderService extends CrudRepositoryService<Order, Long, OrderRepository> {

    public List<Order> getOrdersByCustomer(Long customerId) {
        return super.getRepository().findAllByCustomerId(customerId);
    }
}
@BrowserCallable
@AnonymousAllowed
public class CustomerService extends CrudRepositoryService<Customer, Long, CustomerRepository> { }

Frontend

The master view of the sample application shows all orders in the AutoGrid component. A column for the customer is provided in the table. With the help of a custom render, you can flexibly determine which customer information should be displayed. In the example, the name of the customer is to be displayed.

<AutoGrid
  model={OrderModel}
  service={OrderService}
  columnOptions={{
    customer: {
      renderer: ({ item }: { item: Order }) => <span>{item.customer?.name}</span>,
    },
  }}
/>

AutoGrid - Orders

The AutoGrid component has a great filter mechanism for each column. The column for the customer can also be given an individual filter.

<AutoGrid
  model={OrderModel}
  service={OrderService}
  columnOptions={{
    customer: {
      renderer: ({ item }: { item: Order }) => <span>{item.customer?.name}</span>,
      headerFilterRenderer: ({ setFilter }) => (
        <TextField
          placeholder='Filter...'
          onValueChanged={({ detail }) =>
            setFilter({
              propertyId: 'customer.name',
              filterValue: detail.value,
              matcher: Matcher.CONTAINS,
              '@type': 'propertyString',
            })
          }
        />
      ),
    },
  }}
/>

The detail view of the application shows an order in an AutoForm component so that an order can be created and edited. The field for the customer can also be configured individually here. In this example, it makes sense to display the customer via a Combo Box component. This component enables the selection of a customer, including a helpful filter option.

<AutoForm
  model={OrderModel}
  service={OrderService}
  item={order}
  fieldOptions={{
    customer: {
      renderer: ({ field }) => <ComboBox {...field} items={customers} itemLabelPath='name' />,
    },
  }}
/>

AutoForm - Order

Until now, the 1:n relationship between order and customer was viewed from the perspective of the order. If you want to turn the view around and, for example, view all of a customer’s orders, this can be implemented in the customer’s detail view, for example. The orders can be loaded using the customerId via the individual getOrdersByCustomer method of the OrderService:

  useEffect(() => {
    if (customerId) {
      OrderService.getOrdersByCustomer(Number.parseInt(customerId)).then(setOrders);
    }
  }, [customerId]);

If the orders of a customer are only to be displayed, the Grid component is suitable for this.

<Grid
 items={orders}
>
 <GridColumn path='number' />
 <GridColumn path='created' />
</Grid>

Grid - Orders by customer

Summary

The combination of Spring Boot and Spring Data JPA in the backend and the UI components and the code generation of Hilla in the frontend enable a very productive and at the same time very flexible development of business web apps that contain entities with common relationships such as 1:1 or 1:n.