1:n-Beziehungen in Hilla mit AutoGrid und AutoForm

René Wilby | 19.04.2024 Min. Lesezeit

Entitäten in Geschäftsanwendungen

Bei der Entwicklung von Geschäftsanwendungen spielen Entitäten eine wichtige Rolle für mich. Bevor ich mit der Entwicklung beginne, denke ich zunächst über die beteiligten fachlichen Entitäten und ihre Beziehungen zueinander nach. Ein Entity-Relationship-Modell ist dabei ein nützliches Hilfsmittel, um die Beziehungen zwischen Entitäten zu beschreiben und grafisch darzustellen. Eine sehr typische Beziehung zwischen Entitäten ist die 1:n-Beziehung. Bei einer E-Commerce-Anwendung hat bspw. ein Kunde keine, eine oder beliebig viele Bestellungen und eine Bestellung ist in der Regel genau einem Kunden zugeordnet. Ein anderes Beispiel wäre die Beziehung zwischen einer Rechnung und den enthaltenen Rechnungspositionen: Eine Rechnung besteht aus einer oder mehreren Rechnungspositionen und eine Rechnungsposition ist genau einer Rechnung zugeordnet.

Ein passender Technologie-Stack

Bei der Entwicklung von Geschäftsanwendungen ist es daher wichtig, dass die verwendeten Frameworks diese typische Beziehung möglichst komfortabel und flexibel unterstützen. Bei der Entwicklung von Geschäftsanwendungen mit Hilla werden diese Anforderungen sehr gut erfüllt.

Im Backend kommt Spring Boot zum Einsatz. In Verbindung mit relationalen Datenbanken kann man Spring Data JPA einsetzen und dabei viele Vorteile nutzen, dazu zählen unter anderem:

  • Das Datenbank-Schema kann automatisch aus den Klassen der Entitäten generiert werden.
  • Die Kommunikation mit der Datenbank kann über die sehr mächtigen JPA-Repositories erfolgen.

Im Frontend kann man die Anwendung mit Hilfe der fertigen UI-Komponenten von Hilla sehr schnell entwickeln. So kann ein typisches Master-Detail-UI-Pattern mit Hilfe der Komponenten AutoGrid und AutoForm schnell umgesetzt werden.

Besonderheiten der 1:n-Beziehung

Hilla unterstützt die 1:1-Beziehungen von Entitäten in der AutoGrid-Komponente bereits Out-of-the-box (siehe Beispiel). Für 1:n-Beziehungen kann man sowohl die AutoGrid- als auch die AutoForm-Komponente leicht anpassen, um die Beziehung sinnvoll zu unterstützen.

Beispiel

Im nachfolgenden Beispiel soll dies anhand der Entitäten Order und Customer gezeigt werden. Ein Kunde hat keine, eine oder beliebig viele Bestellungen und eine Bestellung ist genau einem Kunden zugeordnet. Wir betrachten im Beispiel zunächst die 1:n-Beziehung aus Sicht der Bestellung.

Backend

Im Backend werden zunächst die beiden Entitäten Order und Customer erstellt.

@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
}

Im Anschluss werden die zugehörigen JPA-Repositories erstellt.

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

Die Kommunikation zwischen Backend und Frontend erfolgt über entsprechende Services für Order und Customer, die mit @BrowserCallable annotiert werden. Dies genügt, damit Hilla die erforderlichen TypeScript-Klassen generiert, die man anschließend im Frontend verwenden kann.

@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

Die Master-View der Beispiel-Anwendung zeigt alle Bestellungen in der AutoGrid-Komponente. In der Tabelle ist eine Spalte für den Kunden vorgesehen. Mit Hilfe eines eigenen Renderes kann man flexibel bestimmen, welche Informationen des Kunden angezeigt werden sollen. Im Beispiel soll der Name des Kunden angezeigt werden.

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

AutoGrid - Bestellungen

Die AutoGrid-Komponente verfügt über einen tollen Filter-Mechanismus für jede Spalte. Auch die Spalte für den Kunden kann einen individuellen Filter erhalten.

<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',
            })
          }
        />
      ),
    },
  }}
/>

Die Detail-View der Anwendung zeigt eine Bestellung in einer AutoForm-Komponente, so dass eine Bestellung erstellt und bearbeitet werden kann. Auch hier kann das Feld für den Kunden individuell konfiguriert werden. Im vorliegenden Beispiel bietet es sich an, den Kunden über eine Combo Box-Komponente darzustellen. Diese Komponente ermöglicht die Auswahl eines Kunden, inkl. einer hilfreichen Filtermöglichkeit.

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

AutoForm - Bestellung

Bisher wurde die 1:n-Beziehung zwischen Bestellung und Kunde aus der Sicht der Bestellung betrachtet. Möchte man die Betrachtung umdrehen und bspw. alle Bestellungen eines Kunden ansehen, kann dies bspw. in der Detail-View des Kunden implementiert werden. Die Bestellungen können anhand der customerId über die individuelle Methode getOrdersByCustomer des OrderService geladen werden:

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

Sollen die Bestellungen eines Kunden lediglich dargestellt werden, eignet sich die Grid-Komponente dafür.

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

Grid - Bestellungen eines Kunden

Fazit

Die Kombination aus Spring Boot und Spring Data JPA im Backend und der UI-Komponenten und der Code-Generierung von Hilla im Frontend ermöglichen eine sehr produktive und gleichzeitig sehr flexible Entwicklung von Geschäftsanwendungen, die Entitäten mit gängigen Beziehungen, wie 1:1 oder 1:n enthalten.